Compare commits

...

88 Commits

Author SHA1 Message Date
Jobobby04 0aebe1da43 Release 1.6.2 2021-04-28 14:24:05 -04:00
arkon f45fdca168 Remove app update check on Android 5.x
(cherry picked from commit 13324dd1a1)
2021-04-28 14:01:22 -04:00
Jozef Hollý fc5eb4cccc 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>
(cherry picked from commit ae9bf06b46)
2021-04-27 23:36:57 -04:00
Eugene 8ac309c4ae Theme AMOLEDblue (#280) 2021-04-27 17:57:51 -04:00
Jobobby04 f170446c5f Lint 2021-04-26 18:02:29 -04:00
arkon 643bec9bbb Update issue-closer-action
(cherry picked from commit 5236834911)
2021-04-26 18:02:11 -04:00
Ivan Iskandar 134be3893e 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

(cherry picked from commit bf80dd622c)
2021-04-26 18:01:55 -04:00
Andreas 5855822edd 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

(cherry picked from commit 662b71436e)
2021-04-26 18:01:47 -04:00
arkon 3343b766a2 Minor cleanup to updating download status in Updates
(cherry picked from commit f608cb55eb)
2021-04-26 18:01:39 -04:00
arkon 329d24c7db Don't automatically go to HALF_EXPANDED state for color filter tab (closes #4913)
(cherry picked from commit 6ba82da029)
2021-04-26 18:01:28 -04:00
arkon bdfbc641d9 Reset Incognito Mode on app relaunch (closes #4928)
(cherry picked from commit f407e30b6e)
2021-04-26 18:01:20 -04:00
Ivan Iskandar 6e570d7fad Make the download progress status smoother (#4958)
* Make the download progress status smoother

* Download status icon cleanup

(cherry picked from commit 4e7b8c98f9)
2021-04-26 18:01:11 -04:00
arkon b5d696ebe2 Use popup menus for reader shortcuts instead of toggling through
(cherry picked from commit 5f9574541f)
2021-04-26 18:01:02 -04:00
arkon 5299ae4856 Maybe better handle MAL token expiration
(cherry picked from commit 08a6db7d6e)
2021-04-26 18:00:54 -04:00
arkon a9038831da Downgrade back to stable OkHttp
Maybe fixes some crashes.

(cherry picked from commit b485e1d657)
2021-04-26 18:00:45 -04:00
arkon f1a8132307 Remove "Locked" orientation, replace with explicit orientations
Portrait/Landscape allow sensor, Locked Portrait/Landscape don't.

(cherry picked from commit e8d8621f06)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2021-04-26 18:00:36 -04:00
arkon 76185338bf Make manga and chapter folder name searching case insensitive
(cherry picked from commit 4cefbce7c3)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
2021-04-26 17:58:11 -04:00
arkon bda4aae83d Sanitize source download folder name (fixes #4945)
(cherry picked from commit fa31369f99)
2021-04-26 17:57:05 -04:00
Ivan Iskandar 80bf908133 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

(cherry picked from commit d0bf93ebb7)
2021-04-26 17:56:37 -04:00
arkon 91b49f8a0c Consider sort direction when downloading next n chapters (fixes #4916)
(cherry picked from commit 41a747c7e7)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2021-04-26 17:56:28 -04:00
arkon 80a5a54e60 Consider sort direction when resuming (fixes #4909)
(cherry picked from commit 8882cd4787)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
2021-04-26 17:50:48 -04:00
arkon 3104f3a8b5 Add link to official Facebook page
(cherry picked from commit 68bea8a196)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt
2021-04-26 17:26:46 -04:00
Jobobby04 4fa2c968a9 Fix cleanup orphaned downloads removing valid downloads 2021-04-26 17:24:54 -04:00
Eugene be1e7f28ef SY Rus 1.7.0 (#270)
* Rus 1.7.0

* Fix2

* Fix3

* Fix4

* Fix5
2021-04-19 15:24:58 -04:00
Jobobby04 4118b13e5b Release 1.6.1 2021-04-19 15:19:13 -04:00
Jozef Hollý 7e0f2950c1 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>
(cherry picked from commit 554f890ae3)
2021-04-19 14:59:03 -04:00
arkon a8c4da9e2b Theme BiometricUnlockActivity to avoid flashing light theme
(cherry picked from commit dd1743698f)
2021-04-19 14:58:52 -04:00
arkon 72d315b6ed Include extension loading errors in error logs
(cherry picked from commit b092e98ac9)
2021-04-19 14:58:44 -04:00
arkon b886f0a55a Fix activity leak
(cherry picked from commit 9ee6262aed)
2021-04-19 14:58:36 -04:00
Fernando Maldonado 63fa1ee75e 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>
(cherry picked from commit 24a2d86f41)
2021-04-19 14:58:21 -04:00
Andreas 7d1ad7efb6 [SKIP CI] Update FUNDING.yml (#4907)
(cherry picked from commit b5c5c66336)
2021-04-19 14:57:13 -04:00
arkon 56400febd1 Update LeakCanary
(cherry picked from commit a598ac3993)

# Conflicts:
#	app/build.gradle.kts
2021-04-19 14:56:48 -04:00
arkon aa56698dac 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

(cherry picked from commit cab919d74c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2021-04-19 14:56:08 -04:00
Ivan Iskandar d37b24adb1 Fix source SearchView stuck open until query submitted (#4897)
closes #4850

(cherry picked from commit 60a929b92c)
2021-04-19 14:42:18 -04:00
arkon d3778ac6e1 Clean up ChapterCache (remove Gson, Rx usage)
(cherry picked from commit 356b7c346a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2021-04-19 14:42:04 -04:00
Ivan Iskandar e43777bba7 Themes cleanup (#4894)
(cherry picked from commit ad57fde1c5)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseThemedActivity.kt
#	app/src/main/res/values/themes.xml
2021-04-19 14:35:28 -04:00
arkon 0b3209284a Update KotlinX dependencies
(cherry picked from commit 17f7dea21b)
2021-04-19 14:18:47 -04:00
arkon a1be070e99 Minor cleanup
(cherry picked from commit b40af7c3c6)

# Conflicts:
#	README.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
2021-04-19 14:18:39 -04:00
arkon eda47cd546 Move reading mode toast to default bottom position
Toasts don't block user interaction, so it's probably fine.

(cherry picked from commit 9065362fde)
2021-04-19 14:17:37 -04:00
arkon 54d8748c58 Adjust ActionToolbar positioning
Have I ever mentioned that I hate insets?

(cherry picked from commit ad9bad3d17)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2021-04-19 14:16:22 -04:00
arkon 77c17f2556 Avoid duplicate actions in update notifications
(cherry picked from commit dfd858034f)
2021-04-19 14:15:55 -04:00
arkon d81e4158cb Add clipboard error string
I pulled a Jay and forgot to stage something.

(cherry picked from commit 58ad8fa8c0)
2021-04-19 14:15:43 -04:00
arkon 77061067ee 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

(cherry picked from commit 38610d8a24)
2021-04-19 14:15:33 -04:00
arkon 707af702c1 Avoid rare crash in WebViewActivity
(cherry picked from commit 27cec697bf)
2021-04-19 14:15:24 -04:00
arkon 1b0b98b140 [SKIP CI] Add string for EOL update check message
(cherry picked from commit 024f9a8c76)
2021-04-19 14:15:14 -04:00
arkon 2220b6a91d Follow chapter sort setting for start/resume FAB (closes #1716)
(cherry picked from commit f7cc36f2f0)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
2021-04-19 14:14:59 -04:00
arkon 22b8f51fa3 Double tap Updates to go to Download Queue (closes #4884)
(cherry picked from commit ef5148ebb4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2021-04-19 14:07:22 -04:00
arkon 08f0e515d5 Use DSL for creating chapter description spanned string
(cherry picked from commit 6dbc0a6fd5)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt
2021-04-19 14:06:51 -04:00
arkon 28b4281683 Follow chapter sort setting when downloading next n chapters (closes #4725)
(cherry picked from commit fba3f9d501)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
2021-04-19 14:05:12 -04:00
arkon 676f716fcb Update issue templates
(cherry picked from commit d9f8137362)

# Conflicts:
#	.github/ISSUE_TEMPLATE.md
#	.github/ISSUE_TEMPLATE/bug_report.md
#	.github/ISSUE_TEMPLATE/feature_request.md
2021-04-19 14:02:07 -04:00
arkon 665784a241 Adjust MoreController bottom padding for navbar
(cherry picked from commit 28416489b2)
2021-04-19 14:01:11 -04:00
arkon 0d16609f95 Long press reader settings icon to open color filter tab
Partially addresses #4867

(cherry picked from commit 54a23ddd1f)
2021-04-19 14:01:02 -04:00
arkon b46500c837 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>
(cherry picked from commit 3287ca9cf2)
2021-04-19 14:00:55 -04:00
arkon fb5872ef51 Case insensitive source directory search
(cherry picked from commit a59e134862)

# Conflicts:
#	app/build.gradle.kts
2021-04-19 14:00:44 -04:00
arkon bc28e2d617 Adjust ActionToolbar positioning
(cherry picked from commit 1f8c5b0120)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2021-04-19 14:00:10 -04:00
arkon de0c55117d Minor cleanup
(cherry picked from commit c7f839ea4a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt
2021-04-19 13:59:33 -04:00
arkon 936997b52e Remove toolbar snapping
(cherry picked from commit d981245723)
2021-04-19 13:58:43 -04:00
Ivan Iskandar 885c251fb4 Add navigation bar scrim (#4845)
* Revert "Add navigation bar scrim (closes #4836)"

This reverts commit 2a69d1b0

* Add navigation bar scrim

(cherry picked from commit 1f729f1cb3)
2021-04-19 13:58:32 -04:00
Jobobby04 b8e907cea2 Block merged manga from being able to be migrated 2021-04-15 15:28:28 -04:00
Jobobby04 f4c6b2e09c Fix crash in migration if there are no viable chapter numbers 2021-04-15 15:03:44 -04:00
Jobobby04 13f4bfa7bc Remove source migration action 2021-04-15 14:57:49 -04:00
Jobobby04 780c1e68a6 Rename debug function 2021-04-14 19:01:41 -04:00
Jobobby04 f10944521c Fix build 2021-04-14 18:45:48 -04:00
arkon af5ebeca56 Avoid crash when unknown reading mode is used
(cherry picked from commit b4577d6676)
2021-04-14 18:38:23 -04:00
arkon 01ad3dc92b Handle reader toolbar subtitle getting cut off when text is too big (closes #4843)
(cherry picked from commit 544adb9940)

# Conflicts:
#	app/src/main/res/layout/reader_activity.xml
2021-04-14 18:38:09 -04:00
Jays2Kings 5c1423be86 Migrating manga now also saves more chapter info
Bookmarks, Last Read, Date fetched, and has been read fully (this was already tracked but previously it only took the highest chapters

Closes #151

(cherry picked from commit ee4f3e6586910c8c4d62859c867c2a40e41eef67)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt
2021-04-14 18:20:24 -04:00
Jays2Kings 382c23e0fd Fix webtoon mode not calling OnPageSelected in some cases (in upstream too)
This fix isn't 100% tested, but like 80%.

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

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

also fyi just a slight scroll fixes this issue but still

(cherry picked from commit 88fd6e5c9897d4a528f93dd02cfa2a4c644a799d)
(cherry picked from commit 5f0493f1e5)
2021-04-14 18:18:03 -04:00
arkon 189b15fee6 Edge-to-edge in licenses activity
(cherry picked from commit c749e50bec)
2021-04-14 18:17:55 -04:00
arkon d3d937fe17 Use accent color for edge effect
(cherry picked from commit a4e5e3ece5)
2021-04-14 18:17:48 -04:00
Jobobby04 5af0e7e847 Nullable source name for manga type 2021-04-14 18:16:53 -04:00
Jobobby04 142bdd14b7 Add debug function to fix reader toast crash 2021-04-14 18:16:23 -04:00
Jobobby04 0483097fc3 Cleanup 2021-04-13 18:53:10 -04:00
Jobobby04 a3c26c63d4 LoginSource changes 2021-04-13 18:52:57 -04:00
arkon fbe10151f4 Add navigation bar scrim (closes #4836)
(cherry picked from commit 2a69d1b051)
2021-04-13 18:51:57 -04:00
arkon 92fc5ea4a0 Allow weaker unlock methods in Android 6 - 10 (fixes #4833)
(cherry picked from commit 126e1e2d9d)
2021-04-13 18:51:49 -04:00
arkon d2b620f485 Include debug info in dumped crash logs
(cherry picked from commit 0586e1d3ad)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt
2021-04-13 18:51:40 -04:00
arkon 78aa57579d Allow dismissing download progress notification when paused (closes #4832)
(cherry picked from commit 07cb1c237e)
2021-04-13 18:49:54 -04:00
arkon b0a2d8908f Disallow forced dark mode, such as MIUI's
(cherry picked from commit f4f1efe5fa)
2021-04-13 18:49:45 -04:00
arkon de36cd0626 Fix toolbar elevation in History and Updates
(cherry picked from commit 37fdf4d434)
2021-04-13 18:49:34 -04:00
arkon b322ecd34a Fully expand source filter sheet on show (closes #4455)
(cherry picked from commit 99b46096a4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
2021-04-13 18:49:23 -04:00
arkon 540e234562 Use same non-sticky heading style as Browse for Updates/History (closes #4822)
(cherry picked from commit 12e90ae35e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt
2021-04-13 18:31:18 -04:00
arkon f6be2c7a2a Start download when tapping update notification (closes #4825)
(cherry picked from commit 023311a874)
2021-04-12 15:03:49 -04:00
arkon c5df8725de Fix ActionToolbar bottom offset
(cherry picked from commit 155a4dd463)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2021-04-12 15:03:40 -04:00
arkon 738e2f7cf1 Offset appbar using margin instead (maybe fixes #4819)
(cherry picked from commit 15bed1ac4c)
2021-04-12 15:03:10 -04:00
Tooster 0ac56750c8 Fix LibraryUpdateServiceTest so ./gradlew ... doesn't crash (#4821)
(cherry picked from commit 27f55f8098)
2021-04-12 15:03:01 -04:00
Jobobby04 3fb1b4affa Update readme screens 2021-04-12 14:45:13 -04:00
Jobobby04 2602c49756 Hide dedupe by priority 2021-04-12 14:45:13 -04:00
Eugene d23b3c82ba Rus locale (#262)
* Rus lacale

* pref_left_handed_vertical_seekbar_summary

* Fix 1
2021-04-11 23:47:47 -04:00
189 changed files with 1891 additions and 1513 deletions
-1
View File
@@ -1,2 +1 @@
github: inorichi
ko_fi: inorichi ko_fi: inorichi
+8 -2
View File
@@ -2,9 +2,15 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0) - I have updated:
- I have updated all extensions - To the latest version of the app (stable is v1.6.2)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - 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**
+8 -2
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 v1.6.0) - I have updated:
- I have updated all extensions - To the latest version of the app (stable is v1.6.2)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - 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**
+7 -2
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 v1.6.0) - I have updated:
- I have updated all extensions - To the latest version of the app (stable is v1.6.2)
- 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.7 MiB

After

Width:  |  Height:  |  Size: 489 KiB

+1 -1
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: |
+1 -1
View File
@@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above. This
## Features ## Features
Features of Tachiyomi(original) include: Features of Tachiyomi(original) 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
+7 -7
View File
@@ -34,8 +34,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 = 14 versionCode = 16
versionName = "1.6.0" versionName = "1.6.2"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -160,7 +160,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")
@@ -170,7 +170,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")
@@ -181,7 +181,7 @@ dependencies {
// Disk // Disk
implementation("com.jakewharton:disklrucache:2.0.2") implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.inorichi:unifile:e9ee588") implementation("com.github.tachiyomiorg:unifile:17bec43")
implementation("com.github.junrar:junrar:7.4.0") implementation("com.github.junrar:junrar:7.4.0")
// HTML parser // HTML parser
@@ -267,12 +267,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")
// SY --> // SY -->
// [EXH] Android 7 SSL Workaround // [EXH] Android 7 SSL Workaround
+4 -4
View File
@@ -33,7 +33,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"
@@ -85,7 +85,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" />
@@ -200,7 +200,7 @@
<activity <activity
android:name="exh.ui.intercept.InterceptActivity" android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.EHActivity"> android:theme="@style/Theme.Base">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -374,7 +374,7 @@
</activity> </activity>
<activity <activity
android:name="exh.ui.captcha.BrowserActionActivity" android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.EHActivity" /> android:theme="@style/Theme.Base" />
</application> </application>
</manifest> </manifest>
@@ -82,6 +82,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) {
@@ -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
@@ -141,6 +142,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
} }
@@ -2,8 +2,6 @@ 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.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -15,10 +13,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
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
@@ -48,14 +48,12 @@ class ChapterCache(private val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
/** Google Json class used for parsing JSON files. */ /** Google Json class used for parsing JSON files. */
private val gson: Gson by injectLazy() private val json: Json by injectLazy()
// --> EH // --> EH
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
// <-- EH
/** Cache class used for cache management. */ /** Cache class used for cache management. */
// --> EH
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong()) private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
init { init {
@@ -73,7 +71,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
/** /**
@@ -100,43 +98,19 @@ class ChapterCache(private val context: Context) {
} }
// <-- EH // <-- EH
/**
* 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))
}
} }
} }
@@ -148,7 +122,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
@@ -228,6 +202,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}"
} }
@@ -263,7 +263,7 @@ class DownloadManager(private val context: Context) {
if (removeNonFavorite && !manga.favorite) { if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(manga, source) val mangaFolder = provider.getMangaDir(manga, source)
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0) cleaned += 1 + mangaFolder.listFiles().orEmpty().size
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
return cleaned return cleaned
@@ -284,8 +284,7 @@ class DownloadManager(private val context: Context) {
if (cache.getDownloadCount(manga) == 0) { if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(manga, source) val mangaFolder = provider.getMangaDir(manga, source)
val size = mangaFolder.listFiles()?.size ?: 0 if (!mangaFolder.listFiles().isNullOrEmpty()) {
if (size == 0) {
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
} else { } else {
@@ -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))
@@ -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) ?: mangaDir?.findFile("$it.cbz") } .mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) }
.firstOrNull() .firstOrNull()
} }
@@ -123,14 +123,12 @@ class DownloadProvider(private val context: Context) {
source: Source source: Source
): List<UniFile> { ): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return mangaDir.listFiles()!!.asList().filter { return mangaDir.listFiles().orEmpty().asList().filter {
( chapters.find { chp ->
chapters.find { chp -> getValidChapterDirNames(chp).any { dir ->
getValidChapterDirNames(chp).any { dir -> mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null }
} } == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
} == null
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
} }
} }
// SY <-- // SY <--
@@ -141,7 +139,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())
} }
/** /**
@@ -178,6 +176,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)
) )
@@ -24,6 +24,7 @@ object PreferenceValues {
enum class DarkThemeVariant { enum class DarkThemeVariant {
default, default,
blue, blue,
amoledblue,
amoled, amoled,
red, red,
midnightdusk, midnightdusk,
@@ -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()
@@ -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)
} }
@@ -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,52 +8,26 @@ 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()
} catch (e: Exception) {
Result.failure()
} }
Result.success()
} catch (e: Exception) {
Result.failure()
} }
} }
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object { companion object {
private const val TAG = "UpdateChecker" private const val TAG = "UpdateChecker"
@@ -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),
@@ -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)
} }
} }
@@ -1,17 +1,25 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import android.app.Activity
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
interface LoginSource : Source { interface LoginSource : Source {
val needsLogin: Boolean val requiresLogin: Boolean
val twoFactorAuth: AuthSupport
fun isLogged(): Boolean fun isLogged(): Boolean
fun getLoginDialog(source: Source, activity: Activity): DialogController fun getUsername(): String
suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean fun getPassword(): String
suspend fun login(username: String, password: String, twoFactorCode: String?): Boolean
suspend fun logout(): Boolean suspend fun logout(): Boolean
enum class AuthSupport {
NOT_SUPPORTED,
SUPPORTED,
REQUIRED
}
} }
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
@@ -12,7 +11,6 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -26,7 +24,6 @@ import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
@@ -46,7 +43,6 @@ import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
@@ -180,21 +176,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows() return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
} }
override val needsLogin: Boolean = true override val requiresLogin: Boolean = true
override fun getLoginDialog(source: Source, activity: Activity): DialogController { override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED
return MangadexLoginDialog(source as MangaDex)
}
override fun isLogged(): Boolean { override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrl() val httpUrl = MdUtil.baseUrl.toHttpUrl()
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME } return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
} }
override fun getUsername(): String {
return trackManager.mdList.getUsername()
}
override fun getPassword(): String {
return trackManager.mdList.getPassword()
}
override suspend fun login( override suspend fun login(
username: String, username: String,
password: String, password: String,
twoFactorCode: String twoFactorCode: String?
): Boolean { ): Boolean {
return withIOContext { return withIOContext {
val formBody = FormBody.Builder().apply { val formBody = FormBody.Builder().apply {
@@ -202,7 +204,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
add("login_password", password) add("login_password", password)
add("no_js", "1") add("no_js", "1")
add("remember_me", "1") add("remember_me", "1")
add("two_factor", twoFactorCode) add("two_factor", twoFactorCode ?: "")
} }
runCatching { runCatching {
@@ -223,6 +225,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} else { } else {
throw Exception("Json data was null") throw Exception("Json data was null")
} }
}.also {
preferences.setTrackCredentials(trackManager.mdList, username, password)
} }
} }
} }
@@ -1,68 +1,43 @@
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
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
Values.DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_HotPink
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.amoledblue -> R.style.Theme_Tachiyomi_Dark_AmoledBlue
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Dark_Amoled
DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Dark_Red
DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_Dark_MidnightDusk
DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_Dark_HotPink
} }
) } 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)
} }
} }
@@ -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) {}
@@ -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
@@ -51,10 +50,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)
@@ -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()
@@ -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) {
@@ -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
} }
/** /**
@@ -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
@@ -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
@@ -46,10 +45,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 {
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.latest
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.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -32,23 +31,6 @@ open class LatestController :
*/ */
protected var adapter: LatestAdapter? = null protected var adapter: LatestAdapter? = null
/**
* 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 = LatestControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return applicationContext?.getString(R.string.latest) return applicationContext?.getString(R.string.latest)
} }
@@ -82,6 +64,8 @@ open class LatestController :
onMangaClick(manga) onMangaClick(manga)
} }
override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater)
/** /**
* Called when the view is created * Called when the view is created
* *
@@ -90,6 +74,12 @@ open class LatestController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = LatestAdapter(this) adapter = LatestAdapter(this)
// Create recycler and set adapter. // Create recycler and set adapter.
@@ -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.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -46,18 +45,16 @@ class PreMigrationController(bundle: Bundle? = null) :
override fun getTitle() = view?.context?.getString(R.string.select_sources) override fun getTitle() = view?.context?.getString(R.string.select_sources)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun createBinding(inflater: LayoutInflater) = PreMigrationControllerBinding.inflate(inflater)
binding = PreMigrationControllerBinding.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)
val ourAdapter = adapter ?: MigrationSourceAdapter( val ourAdapter = adapter ?: MigrationSourceAdapter(
getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) }, getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) },
@@ -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 android.widget.Toast import android.widget.Toast
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@@ -84,24 +83,23 @@ class MigrationListController(bundle: Bundle? = null) :
private val throttleManager = EHentaiThrottleManager() private val throttleManager = EHentaiThrottleManager()
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationListControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun getTitle(): String { override fun getTitle(): String {
return resources?.getString(R.string.migration) + " (${adapter?.items?.count { return resources?.getString(R.string.migration) + " (${adapter?.items?.count {
it.manga.migrationStatus != MigrationStatus.RUNNING it.manga.migrationStatus != MigrationStatus.RUNNING
}}/${adapter?.itemCount ?: 0})" }}/${adapter?.itemCount ?: 0})"
} }
override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
setTitle() setTitle()
val config = this.config ?: return val config = this.config ?: return
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.view.MenuItem import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -102,16 +103,30 @@ class MigrationProcessAdapter(
// Update chapters read // Update chapters read
if (MigrationFlags.hasChapters(flags)) { if (MigrationFlags.hasChapters(flags)) {
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read }.maxByOrNull { it.chapter_number }?.chapter_number val maxChapterRead =
if (maxChapterRead != null) { prevMangaChapters.filter { it.read }.maxOfOrNull { it.chapter_number }
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) { val prevHistoryList = db.getHistoryByMangaId(prevManga.id!!).executeAsBlocking()
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) { val historyList = mutableListOf<History>()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber) {
val prevChapter =
prevMangaChapters.find { it.isRecognizedNumber && it.chapter_number == chapter.chapter_number }
if (prevChapter != null) {
chapter.bookmark = prevChapter.bookmark
chapter.read = prevChapter.read
chapter.date_fetch = prevChapter.date_fetch
prevHistoryList.find { it.chapter_id == prevChapter.id }?.let { prevHistory ->
val history = History.create(chapter).apply { last_read = prevHistory.last_read }
historyList.add(history)
}
} else if (maxChapterRead != null && chapter.chapter_number <= maxChapterRead) {
chapter.read = true chapter.read = true
} }
} }
db.insertChapters(dbChapters).executeAsBlocking()
} }
db.insertChapters(dbChapters).executeAsBlocking()
db.updateHistoryLastRead(historyList).executeAsBlocking()
} }
// Update categories // Update categories
if (MigrationFlags.hasCategories(flags)) { if (MigrationFlags.hasCategories(flags)) {
@@ -3,7 +3,6 @@ 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 dev.chrisbanes.insetter.applyInsetter
@@ -52,18 +51,16 @@ 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)
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 = MigrationMangaAdapter(this) adapter = MigrationMangaAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
@@ -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
@@ -42,18 +41,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)
@@ -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
} }
/** /**
@@ -46,7 +46,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
} }
class Holder(view: View, adapter: FlexibleAdapter</* SY --> */ IFlexible<RecyclerView.ViewHolder> /* SY <-- */>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter</* SY --> */ IFlexible<RecyclerView.ViewHolder> /* SY <-- */>) : FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view) private val binding = SectionHeaderItemBinding.bind(view)
init { init {
binding.title.text = view.context.getString(/* SY --> */ R.string.select_a_source_to_migrate_from /* SY <-- */) binding.title.text = view.context.getString(/* SY --> */ R.string.select_a_source_to_migrate_from /* SY <-- */)
@@ -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)
@@ -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
} }
/** /**
@@ -9,7 +9,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.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@@ -88,25 +87,16 @@ class SourceController(bundle: Bundle? = null) :
return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */) return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */)
} }
/** 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)
@@ -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 /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) : class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) :
@@ -52,9 +51,9 @@ class SourceHolder(private val view: View, val adapter: SourceAdapter /* SY -->
binding.pin.isVisible = showPins binding.pin.isVisible = showPins
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)
} }
} }
} }
@@ -61,6 +61,7 @@ import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import exh.widget.preference.MangadexLoginDialog
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -158,10 +159,7 @@ open class BrowseSourceController(bundle: Bundle) :
// SY <-- // SY <--
} }
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)
@@ -182,8 +180,8 @@ open class BrowseSourceController(bundle: Bundle) :
preferences.shownMangaDexSimilarAskDialog().set(true) preferences.shownMangaDexSimilarAskDialog().set(true)
} }
if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) { if (mainSource is LoginSource && mainSource.requiresLogin && !mainSource.isLogged()) {
val dialog = mainSource.getLoginDialog(mainSource, activity!!) val dialog = MangadexLoginDialog(mainSource)
dialog.showDialog(router) dialog.showDialog(router)
} }
// SY <-- // SY <--
@@ -411,6 +409,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("")
} }
@@ -9,6 +9,7 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@@ -37,10 +38,17 @@ class SourceFilterSheet(
// EXH <-- // EXH <--
) : BaseBottomSheetDialog(activity) { ) : BaseBottomSheetDialog(activity) {
private var filterNavView: FilterNavigationView private var filterNavView: FilterNavigationView = FilterNavigationView(
activity,
// SY -->
searches = searches,
source = source,
controller = controller
// SY <--
)
private val sheetBehavior: BottomSheetBehavior<*>
init { init {
filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches, source = source, controller = controller/* SY <-- */)
filterNavView.onFilterClicked = { filterNavView.onFilterClicked = {
onFilterClicked() onFilterClicked()
this.dismiss() this.dismiss()
@@ -56,6 +64,13 @@ class SourceFilterSheet(
// EXH <-- // EXH <--
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<*>>) {
@@ -72,7 +87,15 @@ class SourceFilterSheet(
} }
// SY <-- // SY <--
class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List<EXHSavedSearch> = emptyList(), source: CatalogueSource? = null, controller: BaseController<*>? = null/* SY <-- */) : class FilterNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
// SY -->
searches: List<EXHSavedSearch> = emptyList(),
source: CatalogueSource? = null,
controller: BaseController<*>? = null
// SY <--
) :
SimpleNavigationView(context, attrs) { SimpleNavigationView(context, attrs) {
var onFilterClicked = {} var onFilterClicked = {}
@@ -91,8 +114,13 @@ class SourceFilterSheet(
// SY <-- // SY <--
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null) val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true)
private val binding = SourceFilterSheetBinding.inflate(LayoutInflater.from(context), null, false) private val binding = SourceFilterSheetBinding.inflate(
LayoutInflater.from(context),
null,
false
)
init { init {
// SY --> // SY -->
@@ -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
@@ -51,22 +50,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
@@ -143,6 +127,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.
@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -74,18 +73,6 @@ open class IndexController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
/**
* Initiate the view with [R.layout.latest_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 = IndexControllerBinding.inflate(inflater)
return binding.root
}
override fun getTitle(): String? { override fun getTitle(): String? {
return source!!.name return source!!.name
} }
@@ -139,6 +126,8 @@ open class IndexController :
} }
} }
override fun createBinding(inflater: LayoutInflater) = IndexControllerBinding.inflate(inflater)
/** /**
* Called when the view is created * Called when the view is created
* *
@@ -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)
@@ -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 BiometricTimesController :
return resources?.getString(R.string.biometric_lock_times) return resources?.getString(R.string.biometric_lock_times)
} }
/** 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 BiometricTimesController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = BiometricTimesAdapter(this@BiometricTimesController) adapter = BiometricTimesAdapter(this@BiometricTimesController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -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
@@ -74,21 +73,7 @@ class SortTagController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
/** 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.
@@ -98,6 +83,12 @@ class SortTagController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SortTagAdapter(this@SortTagController) adapter = SortTagAdapter(this@SortTagController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -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
@@ -65,21 +64,7 @@ class RepoController :
return resources?.getString(R.string.action_edit_repos) return resources?.getString(R.string.action_edit_repos)
} }
/** 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.
@@ -89,6 +74,12 @@ class RepoController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = RepoAdapter(this@RepoController) adapter = RepoAdapter(this@RepoController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -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
@@ -66,21 +65,7 @@ class SourceCategoryController :
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.
@@ -90,6 +75,12 @@ class SourceCategoryController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SourceCategoryAdapter(this@SourceCategoryController) adapter = SourceCategoryAdapter(this@SourceCategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
@@ -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()
@@ -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
} }
) )
} }
@@ -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 android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@@ -20,6 +19,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.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
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
@@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
@@ -43,6 +42,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.favorites.FavoritesIntroDialog import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus import exh.favorites.FavoritesSyncStatus
import exh.source.MERGED_SOURCE_ID
import exh.source.PERV_EDEN_EN_SOURCE_ID import exh.source.PERV_EDEN_EN_SOURCE_ID
import exh.source.PERV_EDEN_IT_SOURCE_ID import exh.source.PERV_EDEN_IT_SOURCE_ID
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
@@ -193,14 +193,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()
@@ -473,9 +476,6 @@ class LibraryController(
} }
} }
// SY --> // SY -->
R.id.action_source_migration -> {
router.pushController(MigrationSourcesController().withFadeTransaction())
}
R.id.action_sync_favorites -> { R.id.action_sync_favorites -> {
if (preferences.exhShowSyncIntro().get()) { if (preferences.exhShowSyncIntro().get()) {
activity?.let { FavoritesIntroDialog().show(it) } activity?.let { FavoritesIntroDialog().show(it) }
@@ -542,9 +542,13 @@ class LibraryController(
// SY --> // SY -->
R.id.action_migrate -> { R.id.action_migrate -> {
val skipPre = preferences.skipPreMigration().get() val skipPre = preferences.skipPreMigration().get()
val selectedMangaIds = selectedMangas.mapNotNull { it.id } val selectedMangaIds = selectedMangas.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id }
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds) if (selectedMangaIds.isNotEmpty()) {
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
} else {
activity?.toast(R.string.no_valid_manga)
}
} }
R.id.action_clean -> cleanTitles() R.id.action_clean -> cleanTitles()
R.id.action_push_to_mdlist -> pushToMdList() R.id.action_push_to_mdlist -> pushToMdList()
@@ -14,6 +14,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
@@ -48,6 +49,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 exh.EXHMigrations import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
@@ -123,23 +126,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
@@ -148,6 +155,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() }
@@ -171,14 +187,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()
} }
// SY -->
R.id.nav_updates -> { R.id.nav_updates -> {
if (router.backstack.lastOrNull()?.controller() !is DownloadController) { router.pushController(DownloadController().withFadeTransaction())
val controller = router.getControllerWithTag(id.toString()) as? UpdatesController
controller?.router?.pushController(DownloadController().withFadeTransaction())
}
} }
// SY <--
} }
} }
true true
@@ -500,7 +511,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
@@ -218,8 +218,8 @@ class EditMangaDialog : DialogController {
private fun ChipGroup.setChips(items: List<String>) { private fun ChipGroup.setChips(items: List<String>) {
removeAllViews() removeAllViews()
items.forEach { item -> items.asSequence().map { item ->
val chip = Chip(context).apply { Chip(context).apply {
text = item text = item
isCloseIconVisible = true isCloseIconVisible = true
@@ -228,15 +228,16 @@ class EditMangaDialog : DialogController {
removeView(this) removeView(this)
} }
} }
}.forEach {
addView(chip) addView(it)
} }
val addTagChip = Chip(context).apply { val addTagChip = Chip(context).apply {
setText(R.string.add_tag) setText(R.string.add_tag)
chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp) chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)?.apply {
chipIcon?.setTint(context.getResourceColor(R.attr.colorAccent)) setTint(context.getResourceColor(R.attr.colorAccent))
}
textStartPadding = 0F textStartPadding = 0F
clicks().onEach { clicks().onEach {
@@ -17,7 +17,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 android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -276,18 +275,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
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf() val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
@@ -479,7 +481,7 @@ class MangaController :
// Hide options for non-library manga // Hide options for non-library manga
menu.findItem(R.id.action_edit_categories).isVisible = presenter.manga.favorite && presenter.getCategories().isNotEmpty() menu.findItem(R.id.action_edit_categories).isVisible = presenter.manga.favorite && presenter.getCategories().isNotEmpty()
menu.findItem(R.id.action_edit_cover).isVisible = /* SY --> */ false /* presenter.manga.favorite SY <-- */ menu.findItem(R.id.action_edit_cover).isVisible = /* SY --> */ false /* presenter.manga.favorite SY <-- */
menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite /* SY --> */ && presenter.manga.source != MERGED_SOURCE_ID /* SY <-- */
// SY --> // SY -->
menu.findItem(R.id.action_edit).isVisible = presenter.manga.favorite || isLocalSource menu.findItem(R.id.action_edit).isVisible = presenter.manga.favorite || isLocalSource
@@ -1145,8 +1147,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()
} }
} }
@@ -1270,7 +1271,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
@@ -1302,10 +1302,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
} }
@@ -1427,14 +1423,26 @@ class MangaController :
// OVERFLOW MENU DIALOGS // OVERFLOW MENU DIALOGS
private fun getUnreadChaptersSorted() = /* SY --> */ if (presenter.source.isEhBasedSource()) presenter.chapters private fun getUnreadChaptersSorted(): List<ChapterItem> {
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED } val chapters = presenter.chapters
.distinctBy { it.name } .sortedWith(presenter.getChapterSort())
.sortedBy { it.source_order } .filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
else /* SY <-- */ presenter.chapters .distinctBy { it.name }
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED } // SY -->
.distinctBy { it.name } .let {
.sortedByDescending { it.source_order } if (presenter.source.isEhBasedSource()) {
it.reversed()
} else {
it
}
}
// SY <--
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) {
@@ -846,7 +846,11 @@ class MangaPresenter(
} }
// SY <-- // SY <--
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) }
@@ -861,8 +865,6 @@ class MangaPresenter(
} }
else -> throw NotImplementedError("Unimplemented sorting method") else -> throw NotImplementedError("Unimplemented sorting method")
} }
return observable.toSortedList(sortFunction)
} }
/** /**
@@ -889,11 +891,19 @@ 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? {
val chapters = chapters.sortedWith(getChapterSort())
return if (source.isEhBasedSource()) { return if (source.isEhBasedSource()) {
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0) if (sortDescending()) {
if (chapter?.read == false) chapter else null chapters.firstOrNull()?.takeUnless { it.read }
} else {
chapters.lastOrNull()?.takeUnless { it.read }
}
} else { } else {
chapters.sortedByDescending { it.source_order }.find { !it.read } if (sortDescending()) {
return chapters.findLast { !it.read }
} else {
chapters.find { !it.read }
}
} }
} }
@@ -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) {
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
}
}
binding.errorIcon.isVisible = state == Download.State.ERROR this.state = state
this.progress = progress
} }
} }
@@ -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
@@ -64,8 +64,10 @@ class ChapterHolder(
} else /* SY <-- */ descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload))) } else /* SY <-- */ descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload)))
} }
if ((!chapter.read || (adapter.preserveReadingPosition && manga.isEhBasedManga())) && chapter.last_page_read > 0) { if ((!chapter.read || (adapter.preserveReadingPosition && manga.isEhBasedManga())) && 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)
} }
@@ -61,16 +61,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
} }
} }
) )
@@ -29,7 +29,6 @@ open class BaseChapterHolder(
}, },
onMenuItemClick = { onMenuItemClick = {
adapter.clickListener.deleteChapter(position) adapter.clickListener.deleteChapter(position)
true
} }
) )
} }
@@ -42,7 +42,7 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
listOf( listOf(
"No dedupe", "No dedupe",
"Dedupe by priority", /*"Dedupe by priority",*/
"Show source with most chapters", "Show source with most chapters",
"Show source with highest chapter number" "Show source with highest chapter number"
) )
@@ -53,9 +53,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
binding.dedupeModeSpinner.setSelection( binding.dedupeModeSpinner.setSelection(
when (it.chapterSortMode) { when (it.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0 MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0
MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1 /*MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1*/
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 2 MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 1
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 3 MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 2
else -> 0 else -> 0
} }
) )
@@ -69,9 +69,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
) { ) {
controller.mergeReference?.chapterSortMode = when (position) { controller.mergeReference?.chapterSortMode = when (position) {
0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE 0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY /*1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY*/
2 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS 1 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
3 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER 2 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
} }
xLogD(controller.mergeReference?.chapterSortMode) xLogD(controller.mergeReference?.chapterSortMode)
@@ -85,7 +85,13 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
val mergedMangas = controller.mergedMangas val mergedMangas = controller.mergedMangas
val mangaInfoAdapter: ArrayAdapter<String> = ArrayAdapter(itemView.context, android.R.layout.simple_spinner_item, mergedMangas.map { sourceManager.getOrStub(it.second.mangaSourceId).toString() + " " + it.first?.title }) val mangaInfoAdapter: ArrayAdapter<String> = ArrayAdapter(
itemView.context,
android.R.layout.simple_spinner_item,
mergedMangas.map {
sourceManager.getOrStub(it.second.mangaSourceId).toString() + " " + it.first?.title
}
)
mangaInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) mangaInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.mangaInfoSpinner.adapter = mangaInfoAdapter binding.mangaInfoSpinner.adapter = mangaInfoAdapter
@@ -102,11 +108,16 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
position: Int, position: Int,
id: Long id: Long
) { ) {
controller.mergedMangas.find { mergedManga -> mergedManga.second.id == mergedMangas.getOrNull(position)?.second?.id }?.second?.let { newInfoManga -> val mergedInfoManga = controller.mergedMangas
.find { mergedManga ->
mergedManga.second.id == mergedMangas.getOrNull(position)?.second?.id
}
if (mergedInfoManga != null) {
controller.mergedMangas.onEach { controller.mergedMangas.onEach {
it.second.isInfoManga = false it.second.isInfoManga = false
} }
newInfoManga.isInfoManga = true mergedInfoManga.second.isInfoManga = true
} }
} }
@@ -16,6 +16,7 @@ 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.main.WhatsNewDialogController import eu.kanade.tachiyomi.ui.main.WhatsNewDialogController
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
@@ -87,6 +88,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"
@@ -117,7 +126,7 @@ class AboutController : SettingsController() {
preference { preference {
key = "pref_about_label_original_tachiyomi_github" key = "pref_about_label_original_tachiyomi_github"
title = "Original Tachiyomi GitHub " title = "Original Tachiyomi GitHub "
"https://github.com/tachiyomiorg/tachiyomi".also { "https://github.com/tachiyomiorg".also {
summary = it summary = it
onClick { openInBrowser(it) } onClick { openInBrowser(it) }
} }
@@ -140,6 +149,7 @@ class AboutController : SettingsController() {
.withAboutIconShown(false) .withAboutIconShown(false)
.withAboutVersionShown(false) .withAboutVersionShown(false)
.withLicenseShown(true) .withLicenseShown(true)
.withEdgeToEdge(true)
.start(activity!!) .start(activity!!)
} }
} }
@@ -152,6 +162,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 {
@@ -203,20 +218,10 @@ 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}) activity?.copyToClipboard("Debug information", deviceInfo)
Preview build: $syDebugVersion }
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)
} }
private fun getFormattedBuildTime(): String { private fun getFormattedBuildTime(): String {
@@ -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
@@ -27,7 +31,10 @@ 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 exh.ui.batchadd.BatchAddController import exh.ui.batchadd.BatchAddController
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
@@ -40,6 +47,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
@@ -128,6 +138,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
@@ -154,6 +177,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) {
@@ -11,7 +11,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.MotionEvent import android.view.MotionEvent
@@ -68,6 +67,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
@@ -458,13 +458,18 @@ 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 },
presenter.setMangaViewer(newReadingMode.prefValue) selectedItemId = presenter.getMangaViewer(resolveDefault = false),
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
menuToggleToast?.cancel() presenter.setMangaViewer(newReadingMode.prefValue)
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes) menuToggleToast?.cancel()
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
} }
} }
} }
@@ -474,14 +479,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
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)
menuToggleToast?.cancel() menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes) menuToggleToast = toast(newOrientation.stringRes)
}
} }
} }
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) } preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
@@ -523,6 +532,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setOnClickListener { setOnClickListener {
ReaderSettingsSheet(this@ReaderActivity).show() ReaderSettingsSheet(this@ReaderActivity).show()
} }
setOnLongClickListener {
ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).show()
true
}
} }
// --> EH // --> EH
@@ -718,7 +732,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// EXH <-- // EXH <--
/*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)
}*/ }*/
@@ -885,12 +899,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView()) binding.viewerContainer.addView(newViewer.getView())
// SY --> // SY -->
val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.getOrStub(manga.source).name)) val defaultReaderType = manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name))
if (preferences.useAutoWebtoon().get() && manga.viewer == 0 && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) { if (preferences.useAutoWebtoon().get() && manga.viewer == 0 && defaultReaderType != null && defaultReaderType == ReadingModeType.WEBTOON.prefValue) {
readingModeToast?.cancel() readingModeToast?.cancel()
readingModeToast = this.toast(resources.getString(R.string.eh_auto_webtoon_snack)) { readingModeToast = toast(resources.getString(R.string.eh_auto_webtoon_snack))
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
}
} else if (preferences.showReadingMode()) { } else if (preferences.showReadingMode()) {
// SY <-- // SY <--
showReadingModeToast(presenter.getMangaViewer()) showReadingModeToast(presenter.getMangaViewer())
@@ -946,10 +958,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
} }
private fun showReadingModeToast(mode: Int) { private fun showReadingModeToast(mode: Int) {
val strings = resources.getStringArray(R.array.viewers_selector) try {
readingModeToast?.cancel() val strings = resources.getStringArray(R.array.viewers_selector)
readingModeToast = toast(strings[mode]) { readingModeToast?.cancel()
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0) readingModeToast = toast(strings[mode])
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.e("Unknown reading mode: $mode")
} }
} }
@@ -1178,7 +1192,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
} }
@@ -598,7 +598,7 @@ class ReaderPresenter(
val manga = manga ?: return preferences.defaultViewer() val manga = manga ?: return preferences.defaultViewer()
// SY --> // SY -->
return if (resolveDefault && manga.viewer == 0 && preferences.useAutoWebtoon().get()) { return if (resolveDefault && manga.viewer == 0 && preferences.useAutoWebtoon().get()) {
manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.getOrStub(manga.source).name)) ?: if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer manga.defaultReaderType(manga.mangaType(sourceName = sourceManager.get(manga.source)?.name)) ?: if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
} else if (resolveDefault && manga.viewer == 0) { } else if (resolveDefault && manga.viewer == 0) {
preferences.defaultViewer() preferences.defaultViewer()
} else { } else {
@@ -99,8 +99,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 ->
// SY --> // SY -->
@@ -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()
}
} }
} }
@@ -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(
@@ -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
@@ -250,16 +250,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) {
// SY --> // SY -->
if (readerTheme >= 3) { if (readerTheme >= 3) {
@@ -295,21 +292,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!")
} }
@@ -320,11 +327,7 @@ class PagerPageHolder(
} }
} }
if (page !is InsertPage) { return ImageUtil.splitInHalf(imageStream, side)
onPageSplit()
}
return ImageUtil.splitInHalf(inputStream, side)
} }
private fun onPageSplit() { private fun onPageSplit() {
@@ -385,7 +385,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
} }
fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) { fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) {
adapter.onPageSplit(currentPage, newPage, this::class.java) activity.runOnUiThread {
// Need to insert on UI thread else images will go blank
adapter.onPageSplit(currentPage, newPage, this::class.java)
}
} }
private fun cleanupPageSplit() { private fun cleanupPageSplit() {
@@ -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.
*/ */
@@ -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()
@@ -249,11 +240,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].
*/ */
@@ -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)
} }
} }
} }
@@ -35,7 +35,6 @@ class HistoryAdapter(controller: HistoryController) :
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
} }
interface OnResumeClickListener { interface OnResumeClickListener {
@@ -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)
@@ -18,7 +18,6 @@ class UpdatesAdapter(
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
} }
interface OnCoverClickListener { interface OnCoverClickListener {
@@ -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()
} }
} }
@@ -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()
} }
@@ -29,7 +29,8 @@ import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchUI 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.editTextPreference import eu.kanade.tachiyomi.util.preference.editTextPreference
import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.intListPreference
@@ -40,6 +41,7 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference 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.storage.DiskUtil
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 exh.debug.SettingsDebugController import exh.debug.SettingsDebugController
@@ -47,15 +49,8 @@ import exh.log.EHLogLevel
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -349,31 +344,32 @@ class SettingsAdvancedController : SettingsController() {
private fun cleanupDownloads(removeRead: Boolean, removeNonFavorite: Boolean) { private fun cleanupDownloads(removeRead: Boolean, removeNonFavorite: Boolean) {
if (job?.isActive == true) return if (job?.isActive == true) return
activity?.toast(R.string.starting_cleanup) activity?.toast(R.string.starting_cleanup)
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) { job = launchIO {
val mangaList = db.getMangas().executeAsBlocking() val mangaList = db.getMangas().executeAsBlocking()
val sourceManager: SourceManager = Injekt.get()
val downloadManager: DownloadManager = Injekt.get() val downloadManager: DownloadManager = Injekt.get()
var foldersCleared = 0 var foldersCleared = 0
val sources = sourceManager.getOnlineSources() Injekt.get<SourceManager>().getOnlineSources().forEach { source ->
for (source in sources) {
val mangaFolders = downloadManager.getMangaFolders(source) val mangaFolders = downloadManager.getMangaFolders(source)
val sourceManga = mangaList.filter { it.source == source.id } val sourceManga = mangaList
.asSequence()
.filter { it.source == source.id }
.map { it to DiskUtil.buildValidFilename(it.originalTitle) }
.toList()
for (mangaFolder in mangaFolders) { mangaFolders.forEach mangaFolder@{ mangaFolder ->
val manga = sourceManga.find { it.originalTitle == mangaFolder.name } val manga = sourceManga.find { (_, folderName) -> folderName == mangaFolder.name }?.first
if (manga == null) { if (manga == null) {
// download is orphaned delete it // download is orphaned delete it
foldersCleared += 1 + (mangaFolder.listFiles()?.size ?: 0) foldersCleared += 1 + (mangaFolder.listFiles().orEmpty().size)
mangaFolder.delete() mangaFolder.delete()
continue } else {
val chapterList = db.getChapters(manga).executeAsBlocking()
foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source, removeRead, removeNonFavorite)
} }
val chapterList = db.getChapters(manga).executeAsBlocking()
foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source, removeRead, removeNonFavorite)
} }
} }
launchUI { withUIContext {
val activity = activity ?: return@launchUI val activity = activity ?: return@withUIContext
val cleanupString = val cleanupString =
if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup) if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup)
else resources!!.getQuantityString( else resources!!.getQuantityString(
@@ -389,27 +385,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) } activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
.doOnNext { file -> findPreference(CLEAR_CACHE_KEY)?.summary =
if (chapterCache.removeFileFromCache(file.name)) { resources?.getString(R.string.used_cache, chapterCache.readableSize)
deletedFiles++
} }
} catch (e: Throwable) {
withUIContext { activity?.toast(R.string.cache_delete_error) }
} }
.subscribeOn(Schedulers.io()) }
.observeOn(AndroidSchedulers.mainThread())
.doOnError {
activity?.toast(R.string.cache_delete_error)
}
.doOnCompleted {
activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
findPreference(CLEAR_CACHE_KEY)?.summary =
resources?.getString(R.string.used_cache, chapterCache.readableSize)
}
.subscribe()
} }
class ClearDatabaseDialogController : DialogController() { class ClearDatabaseDialogController : DialogController() {
@@ -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) }
}
} }
@@ -132,6 +132,7 @@ class SettingsGeneralController : SettingsController() {
entriesRes = arrayOf( entriesRes = arrayOf(
R.string.theme_dark_default, R.string.theme_dark_default,
R.string.theme_dark_blue, R.string.theme_dark_blue,
R.string.theme_dark_amoledblue,
R.string.theme_dark_amoled, R.string.theme_dark_amoled,
R.string.theme_dark_red, R.string.theme_dark_red,
R.string.theme_dark_midnightdusk, R.string.theme_dark_midnightdusk,
@@ -140,6 +141,7 @@ class SettingsGeneralController : SettingsController() {
entryValues = arrayOf( entryValues = arrayOf(
Values.DarkThemeVariant.default.name, Values.DarkThemeVariant.default.name,
Values.DarkThemeVariant.blue.name, Values.DarkThemeVariant.blue.name,
Values.DarkThemeVariant.amoledblue.name,
Values.DarkThemeVariant.amoled.name, Values.DarkThemeVariant.amoled.name,
Values.DarkThemeVariant.red.name, Values.DarkThemeVariant.red.name,
Values.DarkThemeVariant.midnightdusk.name, Values.DarkThemeVariant.midnightdusk.name,
@@ -93,11 +93,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"
} }
@@ -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
@@ -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 {
@@ -2,15 +2,19 @@ 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 import exh.syDebugVersion
class CrashLogUtil(private val context: Context) { class CrashLogUtil(private val context: Context) {
@@ -19,31 +23,45 @@ class CrashLogUtil(private val context: Context) {
} }
fun dumpLogs() { fun dumpLogs() {
try { launchIO {
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt") try {
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}") val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
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})
Preview build: $syDebugVersion
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)
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),
@@ -173,3 +173,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()
@@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.util.chapter
class NoChaptersException : Exception()
@@ -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]
}
@@ -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
} }
} }
@@ -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
val clipboard = getSystemService<ClipboardManager>()!! try {
clipboard.setPrimaryClip(ClipData.newPlainText(label, content)) val clipboard = getSystemService<ClipboardManager>()!!
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.
*/ */
@@ -84,15 +84,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
} }
/** /**
@@ -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")
}
}
@@ -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)
} }
@@ -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
@@ -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)
} }
} }
@@ -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)
@@ -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))
} }
} }

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