Compare commits

...

78 Commits

Author SHA1 Message Date
arkon
6aff438a16 Release 0.10.12 2021-04-27 09:28:46 -04:00
arkon
13324dd1a1 Remove app update check on Android 5.x 2021-04-27 09:26:46 -04:00
Jozef Hollý
ae9bf06b46 Weblate translations (#4947)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Albedo <Illiator27@gmail.com>
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: Blue cat <bluecat300@gmail.com>
Co-authored-by: Csíkos Martin Nándor <csikos.martin17@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Losms <krishna.chand67@yahoo.com>
Co-authored-by: Luka Paun <croluxgame@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nestor A. Sanchez <help.toastcode@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Techeira Damián <damian.techeira@mercadolibre.com>
Co-authored-by: Thu Htoo San <kokhantyangon@gmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
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/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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: Albedo <Illiator27@gmail.com>
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: Blue cat <bluecat300@gmail.com>
Co-authored-by: Csíkos Martin Nándor <csikos.martin17@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Losms <krishna.chand67@yahoo.com>
Co-authored-by: Luka Paun <croluxgame@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nestor A. Sanchez <help.toastcode@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Techeira Damián <damian.techeira@mercadolibre.com>
Co-authored-by: Thu Htoo San <kokhantyangon@gmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2021-04-27 09:16:22 -04:00
arkon
5236834911 [SKIP CI] Update issue-closer-action 2021-04-25 15:33:15 -04:00
Ivan Iskandar
bf80dd622c Fix download error icon color tint (#4959)
* Fix download error color tint

* Use progress indicator as download icon border

* Resolve feedback

* Use extension function to set tinted drawable
2021-04-25 11:36:13 -04:00
Andreas
662b71436e Cleanup dual page split (#4956)
* Cleanup Dual Page Split

* Move where images is processed

* Change parameter name to imageStream

* Use available instead of Int.MAX_VALUE

* Update JavaDoc
2021-04-25 11:08:51 -04:00
arkon
f608cb55eb Minor cleanup to updating download status in Updates 2021-04-25 11:01:12 -04:00
arkon
6ba82da029 Don't automatically go to HALF_EXPANDED state for color filter tab (closes #4913) 2021-04-25 10:59:53 -04:00
arkon
f407e30b6e Reset Incognito Mode on app relaunch (closes #4928) 2021-04-25 10:57:14 -04:00
Ivan Iskandar
4e7b8c98f9 Make the download progress status smoother (#4958)
* Make the download progress status smoother

* Download status icon cleanup
2021-04-25 10:42:06 -04:00
arkon
5f9574541f Use popup menus for reader shortcuts instead of toggling through 2021-04-24 19:17:52 -04:00
arkon
08a6db7d6e Maybe better handle MAL token expiration 2021-04-24 16:30:53 -04:00
arkon
b485e1d657 Downgrade back to stable OkHttp
Maybe fixes some crashes.
2021-04-23 22:41:46 -04:00
arkon
e8d8621f06 Remove "Locked" orientation, replace with explicit orientations
Portrait/Landscape allow sensor, Locked Portrait/Landscape don't.
2021-04-23 22:37:43 -04:00
arkon
4cefbce7c3 Make manga and chapter folder name searching case insensitive 2021-04-23 08:44:12 -04:00
arkon
fa31369f99 Sanitize source download folder name (fixes #4945) 2021-04-23 08:43:47 -04:00
Ivan Iskandar
d0bf93ebb7 MainActivity: Show bottom nav when the tab page is changed (#4914)
* MainActivity: Show bottom nav when the tab page is changed

* Revert "MainActivity: Show bottom nav when the tab page is changed"

This reverts commit 27fd73db

* MainActivity: Show bottom nav when the app bar is fully expanded
2021-04-21 17:43:53 -04:00
arkon
41a747c7e7 Consider sort direction when downloading next n chapters (fixes #4916) 2021-04-21 17:41:43 -04:00
arkon
8882cd4787 Consider sort direction when resuming (fixes #4909) 2021-04-21 17:38:46 -04:00
arkon
6676490e09 Remove preview release notes
The GitHub releases contain the commit messages.
2021-04-19 15:30:04 -04:00
arkon
68bea8a196 Add link to official Facebook page 2021-04-19 15:23:20 -04:00
arkon
25995c09a0 [SKIP CI] Update issue templates 2021-04-19 11:24:10 -04:00
arkon
0eb8d7d081 Release 0.10.11 2021-04-19 10:52:52 -04:00
Jozef Hollý
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
arkon
dd1743698f Theme BiometricUnlockActivity to avoid flashing light theme 2021-04-19 10:24:57 -04:00
arkon
b092e98ac9 Include extension loading errors in error logs 2021-04-19 10:18:32 -04:00
arkon
9ee6262aed Fix activity leak 2021-04-19 10:18:32 -04:00
Fernando Maldonado
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
Andreas
b5c5c66336 [SKIP CI] Update FUNDING.yml (#4907) 2021-04-19 09:31:34 -04:00
arkon
7654feb6a8 Fix chapter read status not being migrated (fixes #4892) 2021-04-18 13:07:53 -04:00
arkon
a598ac3993 Update LeakCanary 2021-04-18 12:55:17 -04:00
arkon
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
Ivan Iskandar
60a929b92c Fix source SearchView stuck open until query submitted (#4897)
closes #4850
2021-04-18 11:32:22 -04:00
arkon
356b7c346a Clean up ChapterCache (remove Gson, Rx usage) 2021-04-18 11:30:16 -04:00
Ivan Iskandar
ad57fde1c5 Themes cleanup (#4894) 2021-04-18 11:29:56 -04:00
arkon
17f7dea21b Update KotlinX dependencies 2021-04-17 19:19:08 -04:00
arkon
b40af7c3c6 Minor cleanup 2021-04-17 19:05:35 -04:00
arkon
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
joseph619
d264b03ca1 [SKIP CI] Update README banner image (#4887)
* Delete screens.png

* Add files via upload
2021-04-17 18:49:47 -04:00
arkon
ad9bad3d17 Adjust ActionToolbar positioning
Have I ever mentioned that I hate insets?
2021-04-17 13:07:25 -04:00
arkon
dfd858034f Avoid duplicate actions in update notifications 2021-04-17 12:58:14 -04:00
arkon
58ad8fa8c0 Add clipboard error string
I pulled a Jay and forgot to stage something.
2021-04-17 12:33:04 -04:00
arkon
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
arkon
27cec697bf Avoid rare crash in WebViewActivity 2021-04-17 12:22:58 -04:00
arkon
024f9a8c76 [SKIP CI] Add string for EOL update check message 2021-04-17 11:54:17 -04:00
arkon
f7cc36f2f0 Follow chapter sort setting for start/resume FAB (closes #1716) 2021-04-17 11:38:08 -04:00
arkon
ef5148ebb4 Double tap Updates to go to Download Queue (closes #4884) 2021-04-17 11:13:09 -04:00
arkon
6dbc0a6fd5 Use DSL for creating chapter description spanned string 2021-04-17 11:06:30 -04:00
arkon
fba3f9d501 Follow chapter sort setting when downloading next n chapters (closes #4725) 2021-04-17 10:51:38 -04:00
arkon
d9f8137362 Update issue templates 2021-04-16 23:16:46 -04:00
arkon
28416489b2 Adjust MoreController bottom padding for navbar 2021-04-16 23:10:38 -04:00
arkon
54a23ddd1f Long press reader settings icon to open color filter tab
Partially addresses #4867
2021-04-16 23:06:24 -04:00
arkon
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
arkon
a59e134862 Case insensitive source directory search 2021-04-16 22:27:00 -04:00
arkon
1f8c5b0120 Adjust ActionToolbar positioning 2021-04-16 22:26:41 -04:00
arkon
c7f839ea4a Minor cleanup 2021-04-15 10:09:16 -04:00
arkon
d981245723 Remove toolbar snapping 2021-04-15 10:05:47 -04:00
Ivan Iskandar
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
arkon
b4577d6676 Avoid crash when unknown reading mode is used 2021-04-14 18:03:48 -04:00
arkon
544adb9940 Handle reader toolbar subtitle getting cut off when text is too big (closes #4843) 2021-04-14 08:59:23 -04:00
arkon
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
Jays2Kings
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 88fd6e5c98)
2021-04-14 08:49:48 -04:00
arkon
c749e50bec Edge-to-edge in licenses activity 2021-04-13 22:48:54 -04:00
arkon
a4e5e3ece5 Use accent color for edge effect 2021-04-13 22:48:39 -04:00
arkon
2a69d1b051 Add navigation bar scrim (closes #4836) 2021-04-13 18:23:06 -04:00
arkon
126e1e2d9d Allow weaker unlock methods in Android 6 - 10 (fixes #4833) 2021-04-13 15:02:57 -04:00
arkon
0586e1d3ad Include debug info in dumped crash logs 2021-04-13 09:06:41 -04:00
arkon
07cb1c237e Allow dismissing download progress notification when paused (closes #4832) 2021-04-13 08:53:46 -04:00
arkon
f4f1efe5fa Disallow forced dark mode, such as MIUI's 2021-04-13 08:51:08 -04:00
arkon
37fdf4d434 Fix toolbar elevation in History and Updates 2021-04-12 18:43:22 -04:00
arkon
99b46096a4 Fully expand source filter sheet on show (closes #4455) 2021-04-12 17:30:44 -04:00
arkon
12e90ae35e Use same non-sticky heading style as Browse for Updates/History (closes #4822) 2021-04-12 17:11:47 -04:00
arkon
023311a874 Start download when tapping update notification (closes #4825) 2021-04-12 13:43:46 -04:00
arkon
155a4dd463 Fix ActionToolbar bottom offset 2021-04-12 12:42:07 -04:00
arkon
15bed1ac4c Offset appbar using margin instead (maybe fixes #4819) 2021-04-12 09:01:11 -04:00
Tooster
27f55f8098 Fix LibraryUpdateServiceTest so ./gradlew ... doesn't crash (#4821) 2021-04-12 08:35:00 -04:00
arkon
00598879e2 Insets fix for migration manga list 2021-04-11 22:57:54 -04:00
arkon
df274a0a78 Always create releases as draft 2021-04-11 18:40:54 -04:00
160 changed files with 1470 additions and 1141 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

@@ -2,9 +2,15 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.10) - I have updated:
- I have updated all extensions - To the latest version of the app (stable is v0.10.12)
- 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 - 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** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**

View File

@@ -9,9 +9,15 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.10) - I have updated:
- I have updated all extensions - To the latest version of the app (stable is v0.10.12)
- 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 - 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** **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**

View File

@@ -9,9 +9,14 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.10) - I have updated:
- I have updated all extensions - To the latest version of the app (stable is v0.10.12)
- All extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- 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** **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

@@ -88,7 +88,7 @@ jobs:
MD5: ${{ env.APK_MD5 }} MD5: ${{ env.APK_MD5 }}
files: | files: |
tachiyomi-${{ env.VERSION_TAG }}.apk tachiyomi-${{ env.VERSION_TAG }}.apk
draft: ${{ github.event.inputs.dry-run != '' }} draft: true
prerelease: false prerelease: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Autoclose issues - name: Autoclose issues
uses: arkon/issue-closer-action@v3.0 uses: arkon/issue-closer-action@v3.1
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: | rules: |

View File

@@ -1,31 +0,0 @@
### r2903
- The MyAnimeList tracker was rewritten. You will need to log out and log in again.
### r1810
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
run properly. This includes app updates, library updates, and automatic backups.
### r1340
- A new screen for managing extensions was added. If you previously installed extensions from FDroid,
you will have to uninstall all of them first (tap on the extension then uninstall), otherwise you won't be able
to update them due to signature mismatch. You won't lose anything in this process as the extensions themselves
don't store anything.
### r959
- The download manager has been rewritten and it's possible some of your downloads
aren't recognized anymore. You may have to check your downloads folder and manually delete those.
- You can now download to any folder in your SD card.
- The download directory setting has been reset.
### r857
- **Important!** Delete after read has been updated.
This means the value has been reset set to disabled.
This can be changed in Settings > Downloads
### r736
- **Important!** Now chapters follow the order of the sources. **It's required that you update your entire library
before reading in order for them to be synced.** Old behavior can be restored for a manga in the overflow menu of the chapters tab.
### r724
- Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
from the info tab, or clearing the database (the latter won't fix covers from library manga).

View File

@@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above.
## Features ## Features
Features include: 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 * Local reading of downloaded manga
* A configurable reader with multiple viewers, reading directions and other settings. * 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 * [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support

View File

@@ -29,8 +29,8 @@ android {
minSdkVersion(AndroidConfig.minSdk) minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk) targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 57 versionCode = 59
versionName = "0.10.10" versionName = "0.10.12"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -153,7 +153,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0") implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client // Network client
val okhttpVersion = "5.0.0-alpha.2" val okhttpVersion = "4.9.1"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@@ -163,7 +163,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1") implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON // 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-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6") implementation("com.google.code.gson:gson:2.8.6")
@@ -174,7 +174,7 @@ dependencies {
// Disk // Disk
implementation("com.jakewharton:disklrucache:2.0.2") implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.tachiyomiorg:unifile:e9e3a40") implementation("com.github.tachiyomiorg:unifile:17bec43")
implementation("com.github.junrar:junrar:7.4.0") implementation("com.github.junrar:junrar:7.4.0")
// HTML parser // HTML parser
@@ -260,12 +260,12 @@ dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN)) 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-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6") // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
} }
tasks { tasks {

View File

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

View File

@@ -52,6 +52,9 @@ open class App : Application(), LifecycleObserver {
LocaleHelper.updateConfiguration(this, resources.configuration) LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
// Reset Incognito Mode on relaunch
preferences.incognitoMode().set(false)
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {

View File

@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.os.Build
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
@@ -139,6 +140,15 @@ object Migrations {
} }
} }
} }
if (oldVersion < 59) {
// Reset rotation to Free after replacing Lock
preferences.rotation().set(1)
// Disable update check for Android 5.x users
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
UpdaterJob.cancelTask(context)
}
}
return true return true
} }

View File

@@ -2,17 +2,17 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Response import okhttp3.Response
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -42,8 +42,7 @@ class ChapterCache(private val context: Context) {
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024 const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
} }
/** Google Json class used for parsing JSON files. */ private val json: Json by injectLazy()
private val gson: Gson by injectLazy()
/** Cache class used for cache management. */ /** Cache class used for cache management. */
private val diskCache = DiskLruCache.open( private val diskCache = DiskLruCache.open(
@@ -56,7 +55,7 @@ class ChapterCache(private val context: Context) {
/** /**
* Returns directory of cache. * Returns directory of cache.
*/ */
val cacheDir: File private val cacheDir: File
get() = diskCache.directory get() = diskCache.directory
/** /**
@@ -71,43 +70,19 @@ class ChapterCache(private val context: Context) {
val readableSize: String val readableSize: String
get() = Formatter.formatFileSize(context, realSize) 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. * Get page list from cache.
* *
* @param chapter the chapter. * @param chapter the chapter.
* @return an observable of the list of pages. * @return the list of pages.
*/ */
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> { fun getPageListFromCache(chapter: Chapter): List<Page> {
return Observable.fromCallable {
// Get the key for the chapter. // Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter)) val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null // Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use { return diskCache.get(key).use {
gson.fromJson<List<Page>>(it.getString(0)) json.decodeFromString(it.getString(0))
}
} }
} }
@@ -119,7 +94,7 @@ class ChapterCache(private val context: Context) {
*/ */
fun putPageListToCache(chapter: Chapter, pages: List<Page>) { fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string. // 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). // Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null 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 { private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}" return "${chapter.manga_id}${chapter.url}"
} }

View File

@@ -28,7 +28,6 @@ internal class DownloadNotifier(private val context: Context) {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false) setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
} }
} }
@@ -84,7 +83,6 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onProgressChange(download: Download) { fun onProgressChange(download: Download) {
with(progressNotificationBuilder) { with(progressNotificationBuilder) {
// Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
clearActions() clearActions()
@@ -116,6 +114,7 @@ internal class DownloadNotifier(private val context: Context) {
} }
setProgress(download.pages!!.size, download.downloadedImages, false) setProgress(download.pages!!.size, download.downloadedImages, false)
setOngoing(true)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS) show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
} }
@@ -130,6 +129,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
setProgress(0, 0, false) setProgress(0, 0, false)
setOngoing(false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))

View File

@@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query. * @param source the source to query.
*/ */
fun findSourceDir(source: Source): UniFile? { fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source)) return downloadsDir.findFile(getSourceDirName(source), true)
} }
/** /**
@@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
*/ */
fun findMangaDir(manga: Manga, source: Source): UniFile? { fun findMangaDir(manga: Manga, source: Source): UniFile? {
val sourceDir = findSourceDir(source) val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga)) return sourceDir?.findFile(getMangaDirName(manga), true)
} }
/** /**
@@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? { fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source) val mangaDir = findMangaDir(manga, source)
return getValidChapterDirNames(chapter).asSequence() return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it) } .mapNotNull { mangaDir?.findFile(it, true) }
.firstOrNull() .firstOrNull()
} }
@@ -115,7 +115,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query. * @param source the source to query.
*/ */
fun getSourceDirName(source: Source): String { fun getSourceDirName(source: Source): String {
return source.toString() return DiskUtil.buildValidFilename(source.toString())
} }
/** /**
@@ -150,6 +150,7 @@ class DownloadProvider(private val context: Context) {
return listOf( return listOf(
getChapterDirName(chapter), getChapterDirName(chapter),
// TODO: remove this
// Legacy chapter directory name used in v0.9.2 and before // Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name) DiskUtil.buildValidFilename(chapter.name)
) )

View File

@@ -11,9 +11,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
private val json: Json by injectLazy() private val json: Json by injectLazy()
private var oauth: OAuth? = null private var oauth: OAuth? = null
set(value) {
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
}
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@@ -24,21 +21,19 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
if (oauth == null) { if (oauth == null) {
oauth = myanimelist.loadOAuth() oauth = myanimelist.loadOAuth()
} }
// Refresh access token if null or expired. // Refresh access token if expired
if (oauth!!.isExpired()) { if (oauth != null && oauth!!.isExpired()) {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use { chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
if (it.isSuccessful) { if (it.isSuccessful) {
setAuth(json.decodeFromString(it.body!!.string())) setAuth(json.decodeFromString(it.body!!.string()))
} }
} }
} }
// Throw on null auth.
if (oauth == null) { if (oauth == null) {
throw Exception("No authentication token") throw Exception("No authentication token")
} }
// Add the authorization header to the original request. // Add the authorization header to the original request
val authRequest = originalRequest.newBuilder() val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}") .addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build() .build()

View File

@@ -7,8 +7,9 @@ data class OAuth(
val refresh_token: String, val refresh_token: String,
val access_token: String, val access_token: String,
val token_type: String, val token_type: String,
val created_at: Long = System.currentTimeMillis(),
val expires_in: Long val expires_in: Long
) { ) {
fun isExpired() = System.currentTimeMillis() > expires_in fun isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000)
} }

View File

@@ -1,9 +1,6 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
@@ -11,51 +8,25 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork(): Result { override fun doWork() = runBlocking {
return runBlocking {
try { try {
val result = GithubUpdateChecker().checkForUpdate() val result = GithubUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) { if (result is UpdateResult.NewUpdate<*>) {
val url = result.release.downloadLink UpdaterNotifier(context).promptUpdate(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() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
Result.failure() Result.failure()
} }
} }
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object { companion object {
private const val TAG = "UpdateChecker" private const val TAG = "UpdateChecker"

View File

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

View File

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

View File

@@ -1,65 +1,39 @@
package eu.kanade.tachiyomi.ui.base.activity package eu.kanade.tachiyomi.ui.base.activity
import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.os.Build import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.R 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 eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseThemedActivity : AppCompatActivity() { abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
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?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme( val isDarkMode = when (preferences.themeMode().get()) {
when { ThemeMode.light -> false
isDarkMode -> darkTheme ThemeMode.dark -> true
else -> lightTheme 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) super.onCreate(savedInstanceState)
} }
} }

View File

@@ -19,7 +19,8 @@ import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) : abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
RestoreViewOnCreateController(bundle) { RestoreViewOnCreateController(bundle) {
lateinit var binding: VB protected lateinit var binding: VB
private set
lateinit var viewScope: CoroutineScope 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 { abstract fun createBinding(inflater: LayoutInflater): VB
return inflateView(inflater, container)
}
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) {} open fun onViewCreated(view: View) {}

View File

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

View File

@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@@ -57,18 +56,16 @@ open class ExtensionController :
return ExtensionPresenter() return ExtensionPresenter()
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater)
binding = ExtensionControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.swipeRefresh.isRefreshing = true binding.swipeRefresh.isRefreshing = true
binding.swipeRefresh.refreshes() binding.swipeRefresh.refreshes()

View File

@@ -4,12 +4,12 @@ import android.annotation.SuppressLint
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) : class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view) private val binding = SectionHeaderItemBinding.bind(view)
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(item: ExtensionGroupItem) { 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. * Returns the layout resource of this item.
*/ */
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header return R.layout.section_header_item
} }
/** /**

View File

@@ -12,7 +12,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.preference.Preference import androidx.preference.Preference
@@ -65,15 +64,9 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater) return ExtensionDetailControllerBinding.inflate(themedInflater)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
} }
override fun createPresenter(): ExtensionDetailsPresenter { override fun createPresenter(): ExtensionDetailsPresenter {
@@ -88,6 +81,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
val extension = presenter.extension ?: return val extension = presenter.extension ?: return
val context = view.context val context = view.context

View File

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

View File

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

View File

@@ -104,25 +104,23 @@ class SearchPresenter(
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters val maxChapterRead = prevMangaChapters
.filter { it.read } .filter { it.read }
.maxByOrNull { it.chapter_number }?.chapter_number .maxOfOrNull { it.chapter_number } ?: 0f
val bookmarkedChapters = prevMangaChapters
.filter { it.bookmark && it.isRecognizedNumber }
.map { it.chapter_number }
if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) { for (chapter in dbChapters) {
if (chapter.isRecognizedNumber) { 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) { if (chapter.chapter_number <= maxChapterRead) {
chapter.read = true chapter.read = true
} }
if (chapter.chapter_number in bookmarkedChapters) {
chapter.bookmark = true
}
} }
} }
db.insertChapters(dbChapters).executeAsBlocking() db.insertChapters(dbChapters).executeAsBlocking()
} }
}
// Update categories // Update categories
if (migrateCategories) { if (migrateCategories) {

View File

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

View File

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

View File

@@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.source
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view) private val binding = SectionHeaderItemBinding.bind(view)
fun bind(item: LangItem) { fun bind(item: LangItem) {
binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context) 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. * Returns the layout resource of this item.
*/ */
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header return R.layout.section_header_item
} }
/** /**

View File

@@ -8,7 +8,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
@@ -67,25 +66,16 @@ class SourceController :
return SourcePresenter() return SourcePresenter()
} }
/** override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater)
* Initiate the view with [R.layout.source_main_controller].
* override fun onViewCreated(view: View) {
* @param inflater used to load the layout xml. super.onViewCreated(view)
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceAdapter(this) adapter = SourceAdapter(this)

View File

@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.setVectorCompat
class SourceHolder(private val view: View, val adapter: SourceAdapter) : 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 binding.pin.isVisible = true
if (item.isPinned) { 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 { } 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

@@ -124,10 +124,7 @@ open class BrowseSourceController(bundle: Bundle) :
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY)) return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater)
binding = SourceControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
@@ -269,6 +266,7 @@ open class BrowseSourceController(bundle: Bundle) :
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) { if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
router.popController(this) router.popController(this)
} else { } else {
nonSubmittedQuery = ""
searchWithQuery("") searchWithQuery("")
} }

View File

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

View File

@@ -6,7 +6,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -50,22 +49,7 @@ open class GlobalSearchController(
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
/** override fun createBinding(inflater: LayoutInflater) = GlobalSearchControllerBinding.inflate(inflater)
* 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)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return presenter.query return presenter.query
@@ -142,6 +126,12 @@ open class GlobalSearchController(
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = GlobalSearchAdapter(this) adapter = GlobalSearchAdapter(this)
// Create recycler and set adapter. // Create recycler and set adapter.

View File

@@ -4,7 +4,6 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -68,21 +67,7 @@ class CategoryController :
return resources?.getString(R.string.action_edit_categories) return resources?.getString(R.string.action_edit_categories)
} }
/** override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
* 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)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
/** /**
* Called after view inflation. Used to initialize the view. * Called after view inflation. Used to initialize the view.
@@ -92,6 +77,12 @@ class CategoryController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = CategoryAdapter(this@CategoryController) adapter = CategoryAdapter(this@CategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)

View File

@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -55,15 +54,7 @@ class DownloadController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = DownloadControllerBinding.inflate(inflater)
binding = DownloadControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun createPresenter(): DownloadPresenter { override fun createPresenter(): DownloadPresenter {
return DownloadPresenter() return DownloadPresenter()
@@ -76,6 +67,12 @@ class DownloadController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Check if download queue is empty and update information accordingly. // Check if download queue is empty and update information accordingly.
setInformationView() setInformationView()

View File

@@ -81,15 +81,14 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
private fun showPopupMenu(view: View) { private fun showPopupMenu(view: View) {
view.popupMenu( view.popupMenu(
R.menu.download_single, menuRes = R.menu.download_single,
{ initMenu = {
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0 findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
findItem(R.id.move_to_bottom).isVisible = findItem(R.id.move_to_bottom).isVisible =
bindingAdapterPosition != adapter.itemCount - 1 bindingAdapterPosition != adapter.itemCount - 1
}, },
{ onMenuItemClick = {
adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this) adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this)
true
} }
) )
} }

View File

@@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
@@ -18,6 +17,7 @@ import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -163,14 +163,17 @@ class LibraryController(
return LibraryPresenter() return LibraryPresenter()
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
binding = LibraryControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
adapter = LibraryAdapter(this) adapter = LibraryAdapter(this)
binding.libraryPager.adapter = adapter binding.libraryPager.adapter = adapter
binding.libraryPager.pageSelections() binding.libraryPager.pageSelections()

View File

@@ -13,6 +13,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceDialogController import androidx.preference.PreferenceDialogController
@@ -49,6 +50,8 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI 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 eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -98,23 +101,27 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
padding(left = true, top = true, right = true) padding(left = true, top = true, right = true)
} }
} }
binding.bottomNav.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.rootFab.applyInsetter { binding.rootFab.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
margin() margin()
} }
} }
binding.bottomNav.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Make sure navigation bar is on bottom when making it transparent // Make sure navigation bar is on bottom before we modify it
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) { if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
// Keep scrim on light theme if windowLightNavigationBar is not available window.navigationBarColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 || isDarkMode) { !InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true)
window.navigationBarColor = Color.TRANSPARENT ) {
Color.TRANSPARENT
} else {
// Set navbar scrim 70% of navigationBarColor
getResourceColor(android.R.attr.navigationBarColor, 0.7F)
} }
} }
insets insets
@@ -123,6 +130,15 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
tabAnimator = ViewHeightAnimator(binding.tabs, 0L) tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav) bottomNavAnimator = ViewHeightAnimator(binding.bottomNav)
// If bottom nav is hidden, make it visible again when the app bar is expanded
binding.appbar.addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
if (verticalOffset == 0) {
showBottomNav(true)
}
}
)
// Set behavior of bottom nav // Set behavior of bottom nav
preferences.hideBottomBar() preferences.hideBottomBar()
.asImmediateFlow { setBottomNavBehaviorOnScroll() } .asImmediateFlow { setBottomNavBehaviorOnScroll() }
@@ -146,6 +162,9 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
val controller = router.getControllerWithTag(id.toString()) as? LibraryController val controller = router.getControllerWithTag(id.toString()) as? LibraryController
controller?.showSettingsSheet() controller?.showSettingsSheet()
} }
R.id.nav_updates -> {
router.pushController(DownloadController().withFadeTransaction())
}
} }
} }
true true
@@ -439,7 +458,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
fun fixViewToBottom(view: View) { fun fixViewToBottom(view: View) {
val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val maxAbsOffset = appBarLayout.measuredHeight - binding.tabs.measuredHeight val maxAbsOffset = appBarLayout.measuredHeight - binding.tabs.measuredHeight
view.translationY = -maxAbsOffset - verticalOffset.toFloat() view.translationY = -maxAbsOffset - verticalOffset.toFloat() + appBarLayout.marginTop
} }
binding.appbar.addOnOffsetChangedListener(listener) binding.appbar.addOnOffsetChangedListener(listener)
fixedViewsToBottom[view] = listener fixedViewsToBottom[view] = listener

View File

@@ -11,7 +11,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.blue import androidx.core.graphics.blue
@@ -199,18 +198,21 @@ class MangaController :
) )
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = MangaControllerBinding.inflate(inflater)
binding = MangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
} }
override fun onViewCreated(view: View) {
super.onViewCreated(view)
if (manga == null || source == null) return if (manga == null || source == null) return
@@ -725,8 +727,7 @@ class MangaController :
fun onChapterDownloadUpdate(download: Download) { fun onChapterDownloadUpdate(download: Download) {
chaptersAdapter?.currentItems?.find { it.id == download.chapter.id }?.let { chaptersAdapter?.currentItems?.find { it.id == download.chapter.id }?.let {
chaptersAdapter?.updateItem(it) chaptersAdapter?.updateItem(it, it.status)
chaptersAdapter?.notifyDataSetChanged()
} }
} }
@@ -850,7 +851,6 @@ class MangaController :
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } 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 // Hide FAB to avoid interfering with the bottom action toolbar
// actionFab?.hide()
actionFab?.isVisible = false actionFab?.isVisible = false
} }
return false return false
@@ -882,10 +882,6 @@ class MangaController :
chaptersAdapter?.clearSelection() chaptersAdapter?.clearSelection()
selectedChapters.clear() selectedChapters.clear()
actionMode = null actionMode = null
// TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton]
// fails to show up properly
// actionFab?.show()
actionFab?.isVisible = true actionFab?.isVisible = true
} }
@@ -1007,10 +1003,17 @@ class MangaController :
// OVERFLOW MENU DIALOGS // OVERFLOW MENU DIALOGS
private fun getUnreadChaptersSorted() = presenter.chapters private fun getUnreadChaptersSorted(): List<ChapterItem> {
val chapters = presenter.chapters
.sortedWith(presenter.getChapterSort())
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED } .filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.distinctBy { it.name } .distinctBy { it.name }
.sortedByDescending { it.source_order } return if (presenter.sortDescending()) {
chapters.reversed()
} else {
chapters
}
}
private fun downloadChapters(choice: Int) { private fun downloadChapters(choice: Int) {
val chaptersToDownload = when (choice) { val chaptersToDownload = when (choice) {

View File

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

View File

@@ -6,42 +6,38 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.ChapterDownloadViewBinding import eu.kanade.tachiyomi.databinding.ChapterDownloadViewBinding
import eu.kanade.tachiyomi.util.view.setVectorCompat
class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { FrameLayout(context, attrs) {
private val binding: ChapterDownloadViewBinding private val binding: ChapterDownloadViewBinding =
ChapterDownloadViewBinding.inflate(LayoutInflater.from(context), this, false)
private var state = Download.State.NOT_DOWNLOADED private var state = Download.State.NOT_DOWNLOADED
private var progress = 0 private var progress = 0
private var downloadIconAnimator: ObjectAnimator? = null private var downloadIconAnimator: ObjectAnimator? = null
private var isAnimating = false
init { init {
binding = ChapterDownloadViewBinding.inflate(LayoutInflater.from(context), this, false)
addView(binding.root) addView(binding.root)
} }
fun setState(state: Download.State, progress: Int = 0) { fun setState(state: Download.State, progress: Int = 0) {
val isDirty = this.state.value != state.value || this.progress != progress val isDirty = this.state.value != state.value || this.progress != progress
this.state = state
this.progress = progress
if (isDirty) { if (isDirty) {
updateLayout() updateLayout(state, progress)
} }
} }
private fun updateLayout() { private fun updateLayout(state: Download.State, progress: Int) {
binding.downloadIconBorder.isVisible = state == Download.State.NOT_DOWNLOADED binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED ||
state == Download.State.DOWNLOADING || state == Download.State.QUEUE
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || state == Download.State.DOWNLOADING if (state == Download.State.DOWNLOADING || state == Download.State.QUEUE) {
if (state == Download.State.DOWNLOADING) { if (downloadIconAnimator == null) {
if (!isAnimating) {
downloadIconAnimator = downloadIconAnimator =
ObjectAnimator.ofFloat(binding.downloadIcon, "alpha", 1f, 0f).apply { ObjectAnimator.ofFloat(binding.downloadIcon, "alpha", 1f, 0f).apply {
duration = 1000 duration = 1000
@@ -49,22 +45,36 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
repeatMode = ObjectAnimator.REVERSE repeatMode = ObjectAnimator.REVERSE
} }
downloadIconAnimator?.start() downloadIconAnimator?.start()
isAnimating = true
} }
} else { downloadIconAnimator?.currentPlayTime = System.currentTimeMillis() % 2000
} else if (downloadIconAnimator != null) {
downloadIconAnimator?.cancel() downloadIconAnimator?.cancel()
downloadIconAnimator = null
binding.downloadIcon.alpha = 1f binding.downloadIcon.alpha = 1f
isAnimating = false
} }
binding.downloadQueued.isVisible = state == Download.State.QUEUE
binding.downloadProgress.isVisible = state == Download.State.DOWNLOADING || binding.downloadProgress.isVisible = state == Download.State.DOWNLOADING ||
(state == Download.State.QUEUE && progress > 0) state == Download.State.NOT_DOWNLOADED || state == Download.State.QUEUE
binding.downloadProgress.progress = progress if (state == Download.State.DOWNLOADING) {
binding.downloadProgress.setProgressCompat(progress, true)
} else {
binding.downloadProgress.setProgressCompat(100, true)
}
binding.downloadedIcon.isVisible = state == Download.State.DOWNLOADED binding.downloadStatusIcon.apply {
if (state == Download.State.DOWNLOADED || state == Download.State.ERROR) {
binding.errorIcon.isVisible = state == Download.State.ERROR isVisible = true
if (state == Download.State.DOWNLOADED) {
setVectorCompat(R.drawable.ic_check_circle_24dp, android.R.attr.textColorPrimary)
} else {
setVectorCompat(R.drawable.ic_error_outline_24dp, R.attr.colorError)
}
} else {
isVisible = false
}
}
this.state = state
this.progress = progress
} }
} }

View File

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

View File

@@ -51,16 +51,12 @@ class ChaptersSettingsSheet(
private fun showPopupMenu(view: View) { private fun showPopupMenu(view: View) {
view.popupMenu( view.popupMenu(
R.menu.default_chapter_filter, menuRes = R.menu.default_chapter_filter,
{ onMenuItemClick = {
}, when (itemId) {
{
when (this.itemId) {
R.id.set_as_default -> { R.id.set_as_default -> {
SetChapterSettingsDialog(presenter.manga).showDialog(router) SetChapterSettingsDialog(presenter.manga).showDialog(router)
true
} }
else -> true
} }
} }
) )

View File

@@ -29,7 +29,6 @@ open class BaseChapterHolder(
}, },
onMenuItemClick = { onMenuItemClick = {
adapter.clickListener.deleteChapter(position) adapter.clickListener.deleteChapter(position)
true
} }
) )
} }

View File

@@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.setting.SettingsController 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.launchNow
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
@@ -78,15 +79,6 @@ class AboutController : SettingsController() {
openInBrowser(url) openInBrowser(url)
} }
} }
if (BuildConfig.DEBUG) {
preference {
key = "pref_about_notices"
titleRes = R.string.notices
onClick {
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md")
}
}
}
preferenceCategory { preferenceCategory {
preference { preference {
@@ -97,6 +89,14 @@ class AboutController : SettingsController() {
onClick { openInBrowser(it) } onClick { openInBrowser(it) }
} }
} }
preference {
key = "pref_about_facebook"
title = "Facebook"
"https://facebook.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference { preference {
key = "pref_about_twitter" key = "pref_about_twitter"
title = "Twitter" title = "Twitter"
@@ -116,15 +116,7 @@ class AboutController : SettingsController() {
preference { preference {
key = "pref_about_github" key = "pref_about_github"
title = "GitHub" title = "GitHub"
"https://github.com/tachiyomiorg/tachiyomi".also { "https://github.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_label_extensions"
titleRes = R.string.label_extensions
"https://github.com/tachiyomiorg/tachiyomi-extensions".also {
summary = it summary = it
onClick { openInBrowser(it) } onClick { openInBrowser(it) }
} }
@@ -138,6 +130,7 @@ class AboutController : SettingsController() {
.withAboutIconShown(false) .withAboutIconShown(false)
.withAboutVersionShown(false) .withAboutVersionShown(false)
.withLicenseShown(true) .withLicenseShown(true)
.withEdgeToEdge(true)
.start(activity!!) .start(activity!!)
} }
} }
@@ -150,6 +143,11 @@ class AboutController : SettingsController() {
private fun checkVersion() { private fun checkVersion() {
if (activity == null) return if (activity == null) return
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
activity?.toast(R.string.update_check_eol)
return
}
activity?.toast(R.string.update_check_look_for_updates) activity?.toast(R.string.update_check_look_for_updates)
launchNow { launchNow {
@@ -201,20 +199,11 @@ class AboutController : SettingsController() {
} }
private fun copyDebugInfo() { private fun copyDebugInfo() {
val deviceInfo = activity?.let {
""" val deviceInfo = CrashLogUtil(it).getDebugInfo()
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?.copyToClipboard("Debug information", deviceInfo)
} }
}
private fun getFormattedBuildTime(): String { private fun getFormattedBuildTime(): String {
return try { return try {

View File

@@ -1,7 +1,11 @@
package eu.kanade.tachiyomi.ui.more package eu.kanade.tachiyomi.ui.more
import android.content.Context import android.content.Context
import android.os.Bundle
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R 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.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@@ -39,6 +46,9 @@ class MoreController :
private var isDownloading: Boolean = false private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0 private var downloadQueueSize: Int = 0
private var untilDestroySubscriptions = CompositeSubscription()
private set
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_more titleRes = R.string.label_more
@@ -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) { private fun initDownloadQueueSummary(preference: Preference) {
// Handle running/paused status change // Handle running/paused status change
DownloadService.runningRelay 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) : private class MoreHeaderPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Preference(context, attrs) { Preference(context, attrs) {

View File

@@ -10,7 +10,6 @@ import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -59,6 +58,7 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.defaultBar import eu.kanade.tachiyomi.util.view.defaultBar
import eu.kanade.tachiyomi.util.view.hideBar import eu.kanade.tachiyomi.util.view.hideBar
import eu.kanade.tachiyomi.util.view.isDefaultBar import eu.kanade.tachiyomi.util.view.isDefaultBar
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.util.view.showBar import eu.kanade.tachiyomi.util.view.showBar
import eu.kanade.tachiyomi.widget.SimpleAnimationListener import eu.kanade.tachiyomi.widget.SimpleAnimationListener
@@ -357,8 +357,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setTooltip(R.string.viewer) setTooltip(R.string.viewer)
setOnClickListener { setOnClickListener {
val newReadingMode = popupMenu(
ReadingModeType.getNextReadingMode(presenter.getMangaViewer(resolveDefault = false)) items = ReadingModeType.values().map { it.prefValue to it.stringRes },
selectedItemId = presenter.getMangaViewer(resolveDefault = false),
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
presenter.setMangaViewer(newReadingMode.prefValue) presenter.setMangaViewer(newReadingMode.prefValue)
menuToggleToast?.cancel() menuToggleToast?.cancel()
@@ -367,14 +371,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
} }
} }
} }
}
// Rotation // Rotation
with(binding.actionRotation) { with(binding.actionRotation) {
setTooltip(R.string.pref_rotation_type) setTooltip(R.string.pref_rotation_type)
setOnClickListener { setOnClickListener {
val newOrientation = popupMenu(
OrientationType.getNextOrientation(preferences.rotation().get(), resources) items = OrientationType.values().map { it.prefValue to it.stringRes },
selectedItemId = preferences.rotation().get(),
) {
val newOrientation = OrientationType.fromPreference(itemId)
preferences.rotation().set(newOrientation.prefValue) preferences.rotation().set(newOrientation.prefValue)
setOrientation(newOrientation.flag) setOrientation(newOrientation.flag)
@@ -383,6 +391,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
menuToggleToast = toast(newOrientation.stringRes) menuToggleToast = toast(newOrientation.stringRes)
} }
} }
}
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) } preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
@@ -414,11 +423,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setOnClickListener { setOnClickListener {
ReaderSettingsSheet(this@ReaderActivity).show() ReaderSettingsSheet(this@ReaderActivity).show()
} }
setOnLongClickListener {
ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).show()
true
}
} }
} }
private fun updateRotationShortcut(preference: Int) { private fun updateRotationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference, resources) val orientation = OrientationType.fromPreference(preference)
binding.actionRotation.setImageResource(orientation.iconRes) binding.actionRotation.setImageResource(orientation.iconRes)
} }
@@ -554,10 +568,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
} }
private fun showReadingModeToast(mode: Int) { private fun showReadingModeToast(mode: Int) {
try {
val strings = resources.getStringArray(R.array.viewers_selector) val strings = resources.getStringArray(R.array.viewers_selector)
readingModeToast?.cancel() readingModeToast?.cancel()
readingModeToast = toast(strings[mode]) { readingModeToast = toast(strings[mode])
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0) } catch (e: ArrayIndexOutOfBoundsException) {
Timber.e("Unknown reading mode: $mode")
} }
} }
@@ -762,7 +778,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* Forces the user preferred [orientation] on the activity. * Forces the user preferred [orientation] on the activity.
*/ */
private fun setOrientation(orientation: Int) { private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation, resources) val newOrientation = OrientationType.fromPreference(orientation)
if (newOrientation.flag != requestedOrientation) { if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag requestedOrientation = newOrientation.flag
} }

View File

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

View File

@@ -1,43 +1,20 @@
package eu.kanade.tachiyomi.ui.reader.setting package eu.kanade.tachiyomi.ui.reader.setting
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R 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) { 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), 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), PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp),
LOCKED_LANDSCAPE(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp), LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp),
PORTRAIT(3, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp), LOCKED_PORTRAIT(4, 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); LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp),
;
companion object { companion object {
fun fromPreference(preference: Int, resources: Resources): OrientationType = when (preference) { fun fromPreference(preference: Int): OrientationType =
2 -> { values().find { it.prefValue == preference } ?: FREE
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

@@ -8,7 +8,10 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.widget.SimpleTabSelectedListener import eu.kanade.tachiyomi.widget.SimpleTabSelectedListener
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
class ReaderSettingsSheet(private val activity: ReaderActivity) : TabbedBottomSheetDialog(activity) { class ReaderSettingsSheet(
private val activity: ReaderActivity,
showColorFilterSettings: Boolean = false,
) : TabbedBottomSheetDialog(activity) {
private val readingModeSettings = ReaderReadingModeSettings(activity) private val readingModeSettings = ReaderReadingModeSettings(activity)
private val generalSettings = ReaderGeneralSettings(activity) private val generalSettings = ReaderGeneralSettings(activity)
@@ -19,7 +22,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : TabbedBottomSh
init { init {
val sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup) val sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup)
sheetBehavior.isFitToContents = false sheetBehavior.isFitToContents = false
sheetBehavior.halfExpandedRatio = 0.5f sheetBehavior.halfExpandedRatio = 0.25f
val filterTabIndex = getTabViews().indexOf(colorFilterSettings) val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() { binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() {
@@ -33,13 +36,12 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : TabbedBottomSh
if (activity.menuVisible != !isFilterTab) { if (activity.menuVisible != !isFilterTab) {
activity.setMenuVisibility(!isFilterTab) activity.setMenuVisibility(!isFilterTab)
} }
// Partially collapse the sheet for better preview
if (isFilterTab) {
sheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
} }
}) })
if (showColorFilterSettings) {
binding.tabs.getTabAt(filterTabIndex)?.select()
}
} }
override fun getTabViews() = listOf( override fun getTabViews() = listOf(

View File

@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.next
enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) { enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
DEFAULT(0, R.string.default_viewer, R.drawable.ic_reader_default_24dp), DEFAULT(0, R.string.default_viewer, R.drawable.ic_reader_default_24dp),
@@ -17,11 +16,6 @@ enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @D
companion object { companion object {
fun fromPreference(preference: Int): ReadingModeType = values().find { it.prefValue == preference } ?: DEFAULT fun fromPreference(preference: Int): ReadingModeType = values().find { it.prefValue == preference } ?: DEFAULT
fun getNextReadingMode(preference: Int): ReadingModeType {
val current = fromPreference(preference)
return current.next()
}
fun isPagerType(preference: Int): Boolean { fun isPagerType(preference: Int): Boolean {
val mode = fromPreference(preference) val mode = fromPreference(preference)
return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL

View File

@@ -235,16 +235,13 @@ class PagerPageHolder(
readImageHeaderSubscription = Observable readImageHeaderSubscription = Observable
.fromCallable { .fromCallable {
val stream = streamFn().buffered(16) val stream = streamFn().buffered(16)
openStream = stream openStream = process(stream)
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
openStream = processDualPageSplit(openStream!!)
}
if (!isAnimated) { if (!isAnimated) {
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!)) initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!))
} else { } else {
@@ -257,21 +254,31 @@ class PagerPageHolder(
.subscribe({}, {}) .subscribe({}, {})
} }
private fun processDualPageSplit(openStream: InputStream): InputStream { private fun process(imageStream: InputStream): InputStream {
var inputStream = openStream if (!viewer.config.dualPageSplit) {
val (isDoublePage, stream) = when (page) { return imageStream
is InsertPage -> Pair(true, inputStream)
else -> ImageUtil.isDoublePage(inputStream)
} }
inputStream = stream
if (!isDoublePage) return inputStream if (page is InsertPage) {
return splitInHalf(imageStream)
}
val isDoublePage = ImageUtil.isDoublePage(imageStream)
if (!isDoublePage) {
return imageStream
}
onPageSplit()
return splitInHalf(imageStream)
}
private fun splitInHalf(imageStream: InputStream): InputStream {
var side = when { var side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
(viewer is R2LPagerViewer || viewer is VerticalPagerViewer) && page is InsertPage -> ImageUtil.Side.LEFT viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT
(viewer is R2LPagerViewer || viewer is VerticalPagerViewer) && page !is InsertPage -> ImageUtil.Side.RIGHT viewer !is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT
else -> error("We should choose a side!") else -> error("We should choose a side!")
} }
@@ -282,11 +289,7 @@ class PagerPageHolder(
} }
} }
if (page !is InsertPage) { return ImageUtil.splitInHalf(imageStream, side)
onPageSplit()
}
return ImageUtil.splitInHalf(inputStream, side)
} }
private fun onPageSplit() { private fun onPageSplit() {

View File

@@ -385,8 +385,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
} }
fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) { fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) {
activity.runOnUiThread {
// Need to insert on UI thread else images will go blank
adapter.onPageSplit(currentPage, newPage, this::class.java) adapter.onPageSplit(currentPage, newPage, this::class.java)
} }
}
private fun cleanupPageSplit() { private fun cleanupPageSplit() {
adapter.cleanupPageSplit() adapter.cleanupPageSplit()

View File

@@ -281,22 +281,13 @@ class WebtoonPageHolder(
readImageHeaderSubscription = Observable readImageHeaderSubscription = Observable
.fromCallable { .fromCallable {
val stream = streamFn().buffered(16) val stream = streamFn().buffered(16)
openStream = stream openStream = process(stream)
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
val (isDoublePage, stream) = ImageUtil.isDoublePage(openStream!!)
openStream = if (!isDoublePage) {
stream
} else {
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
ImageUtil.splitAndMerge(stream, upperSide)
}
}
if (!isAnimated) { if (!isAnimated) {
val subsamplingView = initSubsamplingImageView() val subsamplingView = initSubsamplingImageView()
subsamplingView.isVisible = true subsamplingView.isVisible = true
@@ -315,6 +306,20 @@ class WebtoonPageHolder(
addSubscription(readImageHeaderSubscription) addSubscription(readImageHeaderSubscription)
} }
private fun process(imageStream: InputStream): InputStream {
if (!viewer.config.dualPageSplit) {
return imageStream
}
val isDoublePage = ImageUtil.isDoublePage(imageStream)
if (!isDoublePage) {
return imageStream
}
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
return ImageUtil.splitAndMerge(imageStream, upperSide)
}
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */

View File

@@ -79,16 +79,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
recycler.addOnScrollListener( recycler.addOnScrollListener(
object : RecyclerView.OnScrollListener() { object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val position = layoutManager.findLastEndVisibleItemPosition() onScrolled()
val item = adapter.items.getOrNull(position)
val allowPreload = checkAllowPreload(item as? ReaderPage)
if (item != null && currentPage != item) {
currentPage = item
when (item) {
is ReaderPage -> onPageSelected(item, allowPreload)
is ChapterTransition -> onTransitionSelected(item)
}
}
if (dy < 0) { if (dy < 0) {
val firstIndex = layoutManager.findFirstVisibleItemPosition() val firstIndex = layoutManager.findFirstVisibleItemPosition()
@@ -243,11 +234,27 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
val position = adapter.items.indexOf(page) val position = adapter.items.indexOf(page)
if (position != -1) { if (position != -1) {
recycler.scrollToPosition(position) recycler.scrollToPosition(position)
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
onScrolled(position)
}
} else { } else {
Timber.d("Page $page not found in adapter") Timber.d("Page $page not found in adapter")
} }
} }
fun onScrolled(pos: Int? = null) {
val position = pos ?: layoutManager.findLastEndVisibleItemPosition()
val item = adapter.items.getOrNull(position)
val allowPreload = checkAllowPreload(item as? ReaderPage)
if (item != null && currentPage != item) {
currentPage = item
when (item) {
is ReaderPage -> onPageSelected(item, allowPreload)
is ChapterTransition -> onTransitionSelected(item)
}
}
}
/** /**
* Scrolls up by [scrollDistance]. * Scrolls up by [scrollDistance].
*/ */

View File

@@ -8,13 +8,13 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.RecentSectionItemBinding import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
import java.util.Date import java.util.Date
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() { class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.recent_section_item return R.layout.section_header_item
} }
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
@@ -39,12 +39,12 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateS
inner class DateSectionItemHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { inner class DateSectionItemHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
private val binding = RecentSectionItemBinding.bind(view) private val binding = SectionHeaderItemBinding.bind(view)
private val now = Date().time private val now = Date().time
fun bind(item: DateSectionItem) { fun bind(item: DateSectionItem) {
binding.sectionText.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS) binding.title.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
} }
} }
} }

View File

@@ -35,7 +35,6 @@ class HistoryAdapter(controller: HistoryController) :
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
} }
interface OnResumeClickListener { interface OnResumeClickListener {

View File

@@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@@ -20,7 +19,6 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@@ -36,13 +34,10 @@ import uy.kohesive.injekt.injectLazy
/** /**
* Fragment that shows recently read manga. * Fragment that shows recently read manga.
* Uses [R.layout.history_controller].
* UI related actions should be called from here.
*/ */
class HistoryController : class HistoryController :
NucleusController<HistoryControllerBinding, HistoryPresenter>(), NucleusController<HistoryControllerBinding, HistoryPresenter>(),
RootController, RootController,
NoToolbarElevationController,
FlexibleAdapter.OnUpdateListener, FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.EndlessScrollListener, FlexibleAdapter.EndlessScrollListener,
HistoryAdapter.OnRemoveClickListener, HistoryAdapter.OnRemoveClickListener,
@@ -76,18 +71,16 @@ class HistoryController :
return HistoryPresenter() return HistoryPresenter()
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
binding = HistoryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter // Initialize adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)

View File

@@ -18,7 +18,6 @@ class UpdatesAdapter(
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
} }
interface OnCoverClickListener { interface OnCoverClickListener {

View File

@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -19,7 +18,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding import eu.kanade.tachiyomi.databinding.UpdatesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@@ -37,13 +35,10 @@ import timber.log.Timber
/** /**
* Fragment that shows recent chapters. * Fragment that shows recent chapters.
* Uses [R.layout.updates_controller].
* UI related actions should be called from here.
*/ */
class UpdatesController : class UpdatesController :
NucleusController<UpdatesControllerBinding, UpdatesPresenter>(), NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
RootController, RootController,
NoToolbarElevationController,
ActionMode.Callback, ActionMode.Callback,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
@@ -75,18 +70,21 @@ class UpdatesController :
return UpdatesPresenter() return UpdatesPresenter()
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = UpdatesControllerBinding.inflate(inflater)
binding = UpdatesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter { binding.recycler.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
padding() padding()
} }
} }
return binding.root binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
} }
override fun onViewCreated(view: View) {
super.onViewCreated(view)
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS) view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
// Init RecyclerView and adapter // Init RecyclerView and adapter
@@ -244,8 +242,7 @@ class UpdatesController :
adapter?.currentItems adapter?.currentItems
?.filterIsInstance<UpdatesItem>() ?.filterIsInstance<UpdatesItem>()
?.find { it.chapter.id == download.chapter.id }?.let { ?.find { it.chapter.id == download.chapter.id }?.let {
adapter?.updateItem(it) adapter?.updateItem(it, it.status)
adapter?.notifyDataSetChanged()
} }
} }

View File

@@ -1,21 +1,19 @@
package eu.kanade.tachiyomi.ui.security package eu.kanade.tachiyomi.ui.security
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity
import eu.kanade.tachiyomi.util.system.BiometricUtil import eu.kanade.tachiyomi.util.system.BiometricUtil
import uy.kohesive.injekt.injectLazy import timber.log.Timber
import java.util.Date import java.util.Date
import java.util.concurrent.Executors import java.util.concurrent.Executors
/** /**
* Blank activity with a BiometricPrompt. * Blank activity with a BiometricPrompt.
*/ */
class BiometricUnlockActivity : AppCompatActivity() { class BiometricUnlockActivity : BaseThemedActivity() {
private val preferences: PreferencesHelper by injectLazy()
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -27,6 +25,7 @@ class BiometricUnlockActivity : AppCompatActivity() {
object : BiometricPrompt.AuthenticationCallback() { object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
Timber.e(errString.toString())
finishAffinity() finishAffinity()
} }

View File

@@ -21,6 +21,8 @@ import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.onChange import eu.kanade.tachiyomi.util.preference.onChange
@@ -32,9 +34,6 @@ import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@@ -172,27 +171,18 @@ class SettingsAdvancedController : SettingsController() {
private fun clearChapterCache() { private fun clearChapterCache() {
if (activity == null) return if (activity == null) return
val files = chapterCache.cacheDir.listFiles() ?: return launchIO {
try {
var deletedFiles = 0 val deletedFiles = chapterCache.clear()
withUIContext {
Observable.defer { Observable.from(files) }
.doOnNext { file ->
if (chapterCache.removeFileFromCache(file.name)) {
deletedFiles++
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError {
activity?.toast(R.string.cache_delete_error)
}
.doOnCompleted {
activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles)) activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
findPreference(CLEAR_CACHE_KEY)?.summary = findPreference(CLEAR_CACHE_KEY)?.summary =
resources?.getString(R.string.used_cache, chapterCache.readableSize) resources?.getString(R.string.used_cache, chapterCache.readableSize)
} }
.subscribe() } catch (e: Throwable) {
withUIContext { activity?.toast(R.string.cache_delete_error) }
}
}
} }
class ClearDatabaseDialogController : DialogController() { class ClearDatabaseDialogController : DialogController() {

View File

@@ -24,9 +24,6 @@ import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -35,19 +32,14 @@ abstract class SettingsController : PreferenceController() {
var preferenceKey: String? = null var preferenceKey: String? = null
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
val viewScope = MainScope() val viewScope = MainScope()
private var themedContext: Context? = null
var untilDestroySubscriptions = CompositeSubscription()
private set
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
if (this is RootController) { if (this is RootController) {
view.updatePadding(bottom = view.context.resources.getDimensionPixelSize(R.dimen.action_toolbar_list_padding)) listView.clipToPadding = false
listView.updatePadding(bottom = view.context.resources.getDimensionPixelSize(R.dimen.action_toolbar_list_padding))
} }
listView.applyInsetter { listView.applyInsetter {
@@ -77,25 +69,31 @@ abstract class SettingsController : PreferenceController() {
} }
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
setHasOptionsMenu(type.isEnter)
super.onChangeStarted(handler, type)
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
untilDestroySubscriptions.unsubscribe() themedContext = null
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val screen = preferenceManager.createPreferenceScreen(getThemedContext()) val tv = TypedValue()
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
themedContext = ContextThemeWrapper(activity, tv.resourceId)
val screen = preferenceManager.createPreferenceScreen(themedContext)
preferenceScreen = screen preferenceScreen = screen
setupPreferenceScreen(screen) setupPreferenceScreen(screen)
} }
abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen
private fun getThemedContext(): Context {
val tv = TypedValue()
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
return ContextThemeWrapper(activity, tv.resourceId)
}
private fun animatePreferenceHighlight(view: View) { private fun animatePreferenceHighlight(view: View) {
ValueAnimator ValueAnimator
.ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.rippleColor)) .ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.rippleColor))
@@ -111,7 +109,7 @@ abstract class SettingsController : PreferenceController() {
return preferenceScreen?.title?.toString() return preferenceScreen?.title?.toString()
} }
fun setTitle() { private fun setTitle() {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController<*> && parentController.getTitle() != null) { if (parentController is BaseController<*> && parentController.getTitle() != null) {
@@ -122,16 +120,4 @@ abstract class SettingsController : PreferenceController() {
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
} }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
setHasOptionsMenu(type.isEnter)
super.onChangeStarted(handler, type)
}
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
} }

View File

@@ -78,11 +78,12 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.pref_rotation_type titleRes = R.string.pref_rotation_type
entriesRes = arrayOf( entriesRes = arrayOf(
R.string.rotation_free, R.string.rotation_free,
R.string.rotation_lock, R.string.rotation_portrait,
R.string.rotation_landscape,
R.string.rotation_force_portrait, R.string.rotation_force_portrait,
R.string.rotation_force_landscape R.string.rotation_force_landscape,
) )
entryValues = arrayOf("1", "2", "3", "4") entryValues = arrayOf("1", "2", "3", "4", "5")
defaultValue = "1" defaultValue = "1"
summary = "%s" summary = "%s"
} }

View File

@@ -6,7 +6,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -33,17 +32,7 @@ class SettingsSearchController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
/** override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater)
* Initiate the view with [R.layout.settings_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 = SettingsSearchControllerBinding.inflate(inflater)
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return presenter.query return presenter.query

View File

@@ -139,8 +139,10 @@ class WebViewActivity : BaseViewBindingActivity<WebviewActivityBinding>() {
} }
override fun onDestroy() { override fun onDestroy() {
binding.webview?.destroy()
super.onDestroy() super.onDestroy()
// Binding sometimes isn't actually instantiated yet somehow
binding?.webview?.destroy()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@@ -2,15 +2,18 @@ package eu.kanade.tachiyomi.util
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import java.io.IOException
class CrashLogUtil(private val context: Context) { class CrashLogUtil(private val context: Context) {
@@ -19,15 +22,31 @@ class CrashLogUtil(private val context: Context) {
} }
fun dumpLogs() { fun dumpLogs() {
launchIO {
try { try {
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt") val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}") Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor()
file.appendText(getDebugInfo())
showNotification(file.getUriCompat(context)) showNotification(file.getUriCompat(context))
} catch (e: IOException) { } catch (e: Throwable) {
context.toast("Failed to get logs") withUIContext { context.toast("Failed to get logs") }
} }
} }
}
fun getDebugInfo(): String {
return """
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()
}
private fun showNotification(uri: Uri) { private fun showNotification(uri: Uri) {
context.notificationManager.cancel(Notifications.ID_CRASH_LOGS) context.notificationManager.cancel(Notifications.ID_CRASH_LOGS)
@@ -35,15 +54,12 @@ class CrashLogUtil(private val context: Context) {
with(notificationBuilder) { with(notificationBuilder) {
setContentTitle(context.getString(R.string.crash_log_saved)) setContentTitle(context.getString(R.string.crash_log_saved))
// Clear old actions if they exist
clearActions() clearActions()
addAction( addAction(
R.drawable.ic_folder_24dp, R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log), context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri) NotificationReceiver.openErrorLogPendingActivity(context, uri)
) )
addAction( addAction(
R.drawable.ic_share_24dp, R.drawable.ic_share_24dp,
context.getString(R.string.action_share), context.getString(R.string.action_share),

View File

@@ -157,3 +157,5 @@ private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: SChapter):
dbChapter.date_upload != sourceChapter.date_upload || dbChapter.date_upload != sourceChapter.date_upload ||
dbChapter.chapter_number != sourceChapter.chapter_number dbChapter.chapter_number != sourceChapter.chapter_number
} }
class NoChaptersException : Exception()

View File

@@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.util.chapter
class NoChaptersException : Exception()

View File

@@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.util.lang
inline fun <reified T : Enum<T>> T.next(): T {
val values = enumValues<T>()
val nextOrdinal = (ordinal + 1) % values.size
return values[nextOrdinal]
}

View File

@@ -1,12 +1,17 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.content.Context import android.content.Context
import android.os.Build
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators import androidx.biometric.BiometricManager.Authenticators
object BiometricUtil { object BiometricUtil {
fun getSupportedAuthenticators(context: Context): Int { fun getSupportedAuthenticators(context: Context): Int {
if (isLegacySecured(context)) {
return Authenticators.BIOMETRIC_WEAK or Authenticators.DEVICE_CREDENTIAL
}
return listOf( return listOf(
Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_STRONG,
Authenticators.BIOMETRIC_WEAK, Authenticators.BIOMETRIC_WEAK,
@@ -17,10 +22,22 @@ object BiometricUtil {
} }
fun isSupported(context: Context): Boolean { fun isSupported(context: Context): Boolean {
return getSupportedAuthenticators(context) != 0 return isLegacySecured(context) || getSupportedAuthenticators(context) != 0
} }
fun isDeviceCredentialAllowed(context: Context): Boolean { fun isDeviceCredentialAllowed(context: Context): Boolean {
return getSupportedAuthenticators(context) and Authenticators.DEVICE_CREDENTIAL != 0 return isLegacySecured(context) || (getSupportedAuthenticators(context) and Authenticators.DEVICE_CREDENTIAL != 0)
}
/**
* Returns whether the device is secured with a PIN, pattern or password.
*/
private fun isLegacySecured(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
if (context.keyguardManager.isDeviceSecure) {
return true
}
}
return false
} }
} }

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.app.ActivityManager import android.app.ActivityManager
import android.app.KeyguardManager
import android.app.Notification import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@@ -33,6 +34,7 @@ import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.truncateCenter import eu.kanade.tachiyomi.util.lang.truncateCenter
import timber.log.Timber
import java.io.File import java.io.File
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -68,10 +70,15 @@ fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toa
fun Context.copyToClipboard(label: String, content: String) { fun Context.copyToClipboard(label: String, content: String) {
if (content.isBlank()) return if (content.isBlank()) return
try {
val clipboard = getSystemService<ClipboardManager>()!! val clipboard = getSystemService<ClipboardManager>()!!
clipboard.setPrimaryClip(ClipData.newPlainText(label, content)) clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50))) toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
} catch (e: Throwable) {
Timber.e(e)
toast(R.string.clipboard_copy_error)
}
} }
/** /**
@@ -153,24 +160,18 @@ val Float.dpToPxEnd: Float
val Resources.isLTR val Resources.isLTR
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
/**
* Property to get the notification manager from the context.
*/
val Context.notificationManager: NotificationManager val Context.notificationManager: NotificationManager
get() = getSystemService()!! get() = getSystemService()!!
/**
* Property to get the connectivity manager from the context.
*/
val Context.connectivityManager: ConnectivityManager val Context.connectivityManager: ConnectivityManager
get() = getSystemService()!! get() = getSystemService()!!
/**
* Property to get the power manager from the context.
*/
val Context.powerManager: PowerManager val Context.powerManager: PowerManager
get() = getSystemService()!! get() = getSystemService()!!
val Context.keyguardManager: KeyguardManager
get() = getSystemService()!!
/** /**
* Convenience method to acquire a partial wake lock. * Convenience method to acquire a partial wake lock.
*/ */

View File

@@ -77,15 +77,20 @@ object ImageUtil {
} }
/** /**
* Check whether the image is a double image (width > height), return the result and original stream * Check whether the image is a double-page spread
* @return true if the width is greater than the height
*/ */
fun isDoublePage(imageStream: InputStream): Pair<Boolean, InputStream> { fun isDoublePage(imageStream: InputStream): Boolean {
imageStream.mark(imageStream.available() + 1)
val imageBytes = imageStream.readBytes() val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
return Pair(options.outWidth > options.outHeight, ByteArrayInputStream(imageBytes)) imageStream.reset()
return options.outWidth > options.outHeight
} }
/** /**

View File

@@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.content.res.Resources
object InternalResourceHelper {
fun getBoolean(context: Context, resName: String, defaultValue: Boolean): Boolean {
val id = getResourceId(resName, "bool")
return if (id != 0) {
context.createPackageContext("android", 0).resources.getBoolean(id)
} else {
defaultValue
}
}
/**
* Get resource id from system resources
* @param resName resource name to get
* @param type resource type of [resName] to get
* @return 0 if not available
*/
private fun getResourceId(resName: String, type: String): Int {
return Resources.getSystem().getIdentifier(resName, type, "android")
}
}

View File

@@ -1,19 +1,21 @@
package eu.kanade.tachiyomi.util.view package eu.kanade.tachiyomi.util.view
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import eu.kanade.tachiyomi.util.system.getResourceColor
/** /**
* Set a vector on a [ImageView]. * Set a vector on a [ImageView].
* *
* @param drawable id of drawable resource * @param drawable id of drawable resource
*/ */
fun ImageView.setVectorCompat(@DrawableRes drawable: Int, tint: Int? = null) { fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? = null) {
val vector = AppCompatResources.getDrawable(context, drawable) val vector = AppCompatResources.getDrawable(context, drawable)
if (tint != null) { if (tint != null) {
vector?.mutate() vector?.mutate()
vector?.setTint(tint) vector?.setTint(context.getResourceColor(tint))
} }
setImageDrawable(vector) setImageDrawable(vector)
} }

View File

@@ -2,6 +2,7 @@
package eu.kanade.tachiyomi.util.view package eu.kanade.tachiyomi.util.view
import android.annotation.SuppressLint
import android.graphics.Point import android.graphics.Point
import android.view.Gravity import android.view.Gravity
import android.view.Menu import android.view.Menu
@@ -9,14 +10,18 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.view.forEach
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
/** /**
* Returns coordinates of view. * Returns coordinates of view.
@@ -63,7 +68,7 @@ inline fun View.setTooltip(@StringRes stringRes: Int) {
inline fun View.popupMenu( inline fun View.popupMenu(
@MenuRes menuRes: Int, @MenuRes menuRes: Int,
noinline initMenu: (Menu.() -> Unit)? = null, noinline initMenu: (Menu.() -> Unit)? = null,
noinline onMenuItemClick: MenuItem.() -> Boolean noinline onMenuItemClick: MenuItem.() -> Unit
): PopupMenu { ): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
popup.menuInflater.inflate(menuRes, popup.menu) popup.menuInflater.inflate(menuRes, popup.menu)
@@ -71,7 +76,50 @@ inline fun View.popupMenu(
if (initMenu != null) { if (initMenu != null) {
popup.menu.initMenu() popup.menu.initMenu()
} }
popup.setOnMenuItemClickListener { it.onMenuItemClick() } popup.setOnMenuItemClickListener {
it.onMenuItemClick()
true
}
popup.show()
return popup
}
/**
* Shows a popup menu on top of this view.
*
* @param items menu item names to inflate the menu with. List of itemId to stringRes pairs.
* @param selectedItemId optionally show a checkmark beside an item with this itemId.
* @param onMenuItemClick function to execute when a menu item is clicked.
*/
@SuppressLint("RestrictedApi")
inline fun View.popupMenu(
items: List<Pair<Int, Int>>,
selectedItemId: Int? = null,
noinline onMenuItemClick: MenuItem.() -> Unit
): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
items.forEach { (id, stringRes) ->
popup.menu.add(0, id, 0, stringRes)
}
if (selectedItemId != null) {
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
val emptyIcon = ContextCompat.getDrawable(context, R.drawable.ic_blank_24dp)
popup.menu.forEach { item ->
item.icon = when (item.itemId) {
selectedItemId -> ContextCompat.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
else -> emptyIcon
}
}
}
popup.setOnMenuItemClickListener {
it.onMenuItemClick()
true
}
popup.show() popup.show()
return popup return popup

View File

@@ -12,7 +12,7 @@ import androidx.annotation.MenuRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CommonActionToolbarBinding import eu.kanade.tachiyomi.databinding.ActionToolbarBinding
/** /**
* A toolbar holding only menu items. * A toolbar holding only menu items.
@@ -20,25 +20,21 @@ import eu.kanade.tachiyomi.databinding.CommonActionToolbarBinding
class ActionToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ActionToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { FrameLayout(context, attrs) {
private val binding: CommonActionToolbarBinding private val binding = ActionToolbarBinding.inflate(LayoutInflater.from(context), this, true)
init {
binding = CommonActionToolbarBinding.inflate(LayoutInflater.from(context), this, true)
}
/** /**
* Remove menu items and remove listener. * Remove menu items and remove listener.
*/ */
fun destroy() { fun destroy() {
binding.commonActionMenu.menu.clear() binding.menu.menu.clear()
binding.commonActionMenu.setOnMenuItemClickListener(null) binding.menu.setOnMenuItemClickListener(null)
} }
/** /**
* Gets a menu item if found. * Gets a menu item if found.
*/ */
fun findItem(@IdRes itemId: Int): MenuItem? { fun findItem(@IdRes itemId: Int): MenuItem? {
return binding.commonActionMenu.menu.findItem(itemId) return binding.menu.menu.findItem(itemId)
} }
/** /**
@@ -46,14 +42,14 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
*/ */
fun show(mode: ActionMode, @MenuRes menuRes: Int, listener: (item: MenuItem?) -> Boolean) { fun show(mode: ActionMode, @MenuRes menuRes: Int, listener: (item: MenuItem?) -> Boolean) {
// Avoid re-inflating the menu // Avoid re-inflating the menu
if (binding.commonActionMenu.menu.size() == 0) { if (binding.menu.menu.size() == 0) {
mode.menuInflater.inflate(menuRes, binding.commonActionMenu.menu) mode.menuInflater.inflate(menuRes, binding.menu.menu)
binding.commonActionMenu.setOnMenuItemClickListener { listener(it) } binding.menu.setOnMenuItemClickListener { listener(it) }
} }
binding.commonActionToolbar.isVisible = true binding.actionToolbar.isVisible = true
val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom) val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom)
binding.commonActionToolbar.startAnimation(bottomAnimation) binding.actionToolbar.startAnimation(bottomAnimation)
} }
/** /**
@@ -64,10 +60,10 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
bottomAnimation.setAnimationListener( bottomAnimation.setAnimationListener(
object : SimpleAnimationListener() { object : SimpleAnimationListener() {
override fun onAnimationEnd(animation: Animation) { override fun onAnimationEnd(animation: Animation) {
binding.commonActionToolbar.isVisible = false binding.actionToolbar.isVisible = false
} }
} }
) )
binding.commonActionToolbar.startAnimation(bottomAnimation) binding.actionToolbar.startAnimation(bottomAnimation)
} }
} }

View File

@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.setting package eu.kanade.tachiyomi.widget
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
@@ -7,15 +8,21 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.ArrayRes import androidx.annotation.ArrayRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.view.forEach
import androidx.core.view.get
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SpinnerPreferenceBinding import eu.kanade.tachiyomi.databinding.SpinnerPreferenceBinding
import eu.kanade.tachiyomi.util.system.getResourceColor
class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { FrameLayout(context, attrs) {
private var entries = emptyList<String>() private var entries = emptyList<String>()
private var selectedPosition = 0
private var popup: PopupMenu? = null private var popup: PopupMenu? = null
var onItemSelectedListener: ((Int) -> Unit)? = null var onItemSelectedListener: ((Int) -> Unit)? = null
@@ -30,17 +37,26 @@ class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: Attri
} }
} }
private val emptyIcon by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_blank_24dp)
}
private val checkmarkIcon by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
}
private val binding = SpinnerPreferenceBinding.inflate(LayoutInflater.from(context), this, false) private val binding = SpinnerPreferenceBinding.inflate(LayoutInflater.from(context), this, false)
init { init {
addView(binding.root) addView(binding.root)
val attr = context.obtainStyledAttributes(attrs, R.styleable.SpinnerPreference) val attr = context.obtainStyledAttributes(attrs, R.styleable.MaterialSpinnerView)
val title = attr.getString(R.styleable.SpinnerPreference_title).orEmpty() val title = attr.getString(R.styleable.MaterialSpinnerView_title).orEmpty()
binding.title.text = title binding.title.text = title
val entries = (attr.getTextArray(R.styleable.SpinnerPreference_android_entries) ?: emptyArray()).map { it.toString() } val entries = (attr.getTextArray(R.styleable.MaterialSpinnerView_android_entries) ?: emptyArray()).map { it.toString() }
this.entries = entries this.entries = entries
binding.details.text = entries.firstOrNull().orEmpty() binding.details.text = entries.firstOrNull().orEmpty()
@@ -48,6 +64,14 @@ class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: Attri
} }
fun setSelection(selection: Int) { fun setSelection(selection: Int) {
popup?.menu?.get(selectedPosition)?.let {
it.icon = emptyIcon
it.title = entries[selectedPosition]
}
selectedPosition = selection
popup?.menu?.get(selectedPosition)?.let {
it.icon = checkmarkIcon
}
binding.details.text = entries.getOrNull(selection).orEmpty() binding.details.text = entries.getOrNull(selection).orEmpty()
} }
@@ -118,11 +142,19 @@ class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: Attri
return pos return pos
} }
@SuppressLint("RestrictedApi")
fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu { fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu {
val popup = PopupMenu(context, this, Gravity.END, R.attr.actionOverflowMenuStyle, 0) val popup = PopupMenu(context, this, Gravity.END, R.attr.actionOverflowMenuStyle, 0)
entries.forEachIndexed { index, entry -> entries.forEachIndexed { index, entry ->
popup.menu.add(0, index, 0, entry) popup.menu.add(0, index, 0, entry)
} }
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
popup.menu.forEach {
it.icon = emptyIcon
}
popup.menu.getItem(selectedPosition)?.let {
it.icon = checkmarkIcon
}
popup.setOnMenuItemClickListener { menuItem -> popup.setOnMenuItemClickListener { menuItem ->
val pos = menuClicked(menuItem) val pos = menuClicked(menuItem)
onItemClick(pos) onItemClick(pos)

View File

@@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.widget.materialdialogs package eu.kanade.tachiyomi.widget.materialdialogs
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setVectorCompat
class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
AppCompatImageView(context, attrs) { AppCompatImageView(context, attrs) {
@@ -19,19 +16,11 @@ class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: Attri
} }
private fun updateDrawable() { private fun updateDrawable() {
val drawable = when (state) { when (state) {
State.UNCHECKED -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal) State.UNCHECKED -> setVectorCompat(R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
State.INDETERMINATE -> tintVector(context, R.drawable.ic_indeterminate_check_box_24dp) State.INDETERMINATE -> setVectorCompat(R.drawable.ic_indeterminate_check_box_24dp, R.attr.colorAccent)
State.CHECKED -> tintVector(context, R.drawable.ic_check_box_24dp) State.CHECKED -> setVectorCompat(R.drawable.ic_check_box_24dp, R.attr.colorAccent)
State.INVERSED -> tintVector(context, R.drawable.ic_check_box_x_24dp) State.INVERSED -> setVectorCompat(R.drawable.ic_check_box_x_24dp, R.attr.colorAccent)
}
setImageDrawable(drawable)
}
private fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
return AppCompatResources.getDrawable(context, resId)!!.apply {
setTint(context.getResourceColor(colorAttrRes))
} }
} }

View File

@@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.widget.ViewPagerAdapter
abstract class TabbedBottomSheetDialog(private val activity: Activity) : BaseBottomSheetDialog(activity) { abstract class TabbedBottomSheetDialog(private val activity: Activity) : BaseBottomSheetDialog(activity) {
val binding: CommonTabbedSheetBinding = CommonTabbedSheetBinding.inflate(activity.layoutInflater) val binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater)
init { init {
val adapter = LibrarySettingsSheetAdapter() val adapter = LibrarySettingsSheetAdapter()

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"
android:thicknessRatio="2">
<solid android:color="@android:color/transparent" />
<size
android:width="25dp"
android:height="25dp" />
<stroke
android:width="2dp"
android:color="?colorAccent" />
</shape>

View File

@@ -0,0 +1,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="24dp"
android:height="24dp" />
<solid android:color="@android:color/transparent" />
</shape>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/black"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@android:color/black" android:fillColor="@android:color/black"
android:pathData="M22.15,13.85H1.85A1.86,1.86,0,0,1,0,12H0a1.86,1.86,0,0,1,1.85-1.85H22.15A1.86,1.86,0,0,1,24,12h0A1.86,1.86,0,0,1,22.15,13.85Z" /> android:pathData="M1.01,7L1,17c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2H3c-1.1,0 -1.99,0.9 -1.99,2zM19,7v10H5V7h14z" />
</vector> </vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M17,1.01L7,1c-1.1,0 -1.99,0.9 -1.99,2v18c0,1.1 0.89,2 1.99,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z" />
</vector>

View File

@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/common_action_toolbar" android:id="@+id/action_toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
@@ -25,7 +25,7 @@
app:contentInsetStart="8dp"> app:contentInsetStart="8dp">
<androidx.appcompat.widget.ActionMenuView <androidx.appcompat.widget.ActionMenuView
android:id="@+id/common_action_menu" android:id="@+id/menu"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

View File

@@ -7,16 +7,6 @@
android:padding="8dp" android:padding="8dp"
android:background="?selectableItemBackgroundBorderless"> android:background="?selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/download_icon_border"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/border_circle"
app:tint="?android:attr/textColorHint"
tools:ignore="ContentDescription" />
<ImageView <ImageView
android:id="@+id/download_icon" android:id="@+id/download_icon"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -32,42 +22,17 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="1dp" android:padding="1dp"
android:visibility="gone" android:progress="100"
app:indicatorColor="?android:attr/textColorHint"
app:indicatorInset="0dp"
app:indicatorSize="24dp"
app:trackThickness="2dp" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/download_queued"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:indeterminate="true"
android:padding="1dp"
android:visibility="gone"
app:indicatorColor="?android:attr/textColorHint" app:indicatorColor="?android:attr/textColorHint"
app:indicatorInset="0dp" app:indicatorInset="0dp"
app:indicatorSize="24dp" app:indicatorSize="24dp"
app:trackThickness="2dp" /> app:trackThickness="2dp" />
<ImageView <ImageView
android:id="@+id/downloaded_icon" android:id="@+id/download_status_icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="fitXY" android:scaleType="fitXY"
android:visibility="gone"
app:srcCompat="@drawable/ic_check_circle_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/error_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:visibility="gone"
app:srcCompat="@drawable/ic_error_outline_24dp"
app:tint="?attr/colorError"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</FrameLayout> </FrameLayout>

View File

@@ -17,7 +17,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="@dimen/action_toolbar_list_padding" android:paddingBottom="@dimen/action_toolbar_list_padding"
tools:listitem="@layout/source_main_controller_card_header" /> tools:listitem="@layout/section_header_item" />
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout> </eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>

View File

@@ -19,7 +19,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:theme="?attr/actionBarTheme" android:theme="?attr/actionBarTheme"
app:layout_scrollFlags="scroll|enterAlways|snap" /> app:layout_scrollFlags="scroll|enterAlways" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"

View File

@@ -60,7 +60,8 @@
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary" /> android:background="?attr/colorPrimary" />
<LinearLayout <LinearLayout

View File

@@ -10,14 +10,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/rotation_mode" android:id="@+id/rotation_mode"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:entries="@array/rotation_type" android:entries="@array/rotation_type"
app:title="@string/pref_rotation_type" /> app:title="@string/pref_rotation_type" />
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/background_color" android:id="@+id/background_color"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -15,28 +15,28 @@
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:textAppearance="@style/TextAppearance.Medium.SubHeading" /> android:textAppearance="@style/TextAppearance.Medium.SubHeading" />
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/pager_nav" android:id="@+id/pager_nav"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:entries="@array/pager_nav" android:entries="@array/pager_nav"
app:title="@string/pref_viewer_nav" /> app:title="@string/pref_viewer_nav" />
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/tapping_inverted" android:id="@+id/tapping_inverted"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:entries="@array/invert_tapping_mode" android:entries="@array/invert_tapping_mode"
app:title="@string/pref_read_with_tapping_inverted" /> app:title="@string/pref_read_with_tapping_inverted" />
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/scale_type" android:id="@+id/scale_type"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:entries="@array/image_scale_type" android:entries="@array/image_scale_type"
app:title="@string/pref_image_scale_type" /> app:title="@string/pref_image_scale_type" />
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/zoom_start" android:id="@+id/zoom_start"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -10,7 +10,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<eu.kanade.tachiyomi.ui.reader.setting.SpinnerPreference <eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/viewer" android:id="@+id/viewer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

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