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
+8 -2
View File
@@ -2,9 +2,15 @@
I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0)
- I have updated all extensions
- I have updated:
- 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
- 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**
+8 -2
View File
@@ -9,9 +9,15 @@ labels: "bug"
I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0)
- I have updated all extensions
- I have updated:
- 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
- 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**
+7 -2
View File
@@ -9,9 +9,14 @@ labels: "feature"
I acknowledge that:
- I have updated to the latest version of the app (stable is v1.6.0)
- I have updated all extensions
- I have updated:
- 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
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 489 KiB

+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.0
uses: arkon/issue-closer-action@v3.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
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 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
* 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
+7 -7
View File
@@ -34,8 +34,8 @@ android {
minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 14
versionName = "1.6.0"
versionCode = 16
versionName = "1.6.2"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -160,7 +160,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client
val okhttpVersion = "5.0.0-alpha.2"
val okhttpVersion = "4.9.1"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@@ -170,7 +170,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON
val kotlinSerializationVersion = "1.0.1"
val kotlinSerializationVersion = "1.1.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6")
@@ -181,7 +181,7 @@ dependencies {
// Disk
implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.tachiyomiorg:unifile:17bec43")
implementation("com.github.junrar:junrar:7.4.0")
// HTML parser
@@ -267,12 +267,12 @@ dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
val coroutinesVersion = "1.4.2"
val coroutinesVersion = "1.4.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
// SY -->
// [EXH] Android 7 SSL Workaround
+4 -4
View File
@@ -33,7 +33,7 @@
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Tachiyomi.Light"
android:theme="@style/Theme.Base"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".ui.main.MainActivity"
@@ -85,7 +85,7 @@
</activity>
<activity
android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Splash" />
android:theme="@style/Theme.Base" />
<activity
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" />
@@ -200,7 +200,7 @@
<activity
android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name"
android:theme="@style/Theme.EHActivity">
android:theme="@style/Theme.Base">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -374,7 +374,7 @@
</activity>
<activity
android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.EHActivity" />
android:theme="@style/Theme.Base" />
</application>
</manifest>
@@ -82,6 +82,9 @@ open class App : Application(), LifecycleObserver {
LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
// Reset Incognito Mode on relaunch
preferences.incognitoMode().set(false)
}
override fun attachBaseContext(base: Context) {
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi
import android.os.Build
import androidx.core.content.edit
import androidx.preference.PreferenceManager
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
}
@@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -15,10 +13,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Response
import okio.buffer
import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
@@ -48,14 +48,12 @@ class ChapterCache(private val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
/** Google Json class used for parsing JSON files. */
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
// --> EH
private val prefs: PreferencesHelper by injectLazy()
// <-- EH
/** Cache class used for cache management. */
// --> EH
private var diskCache = setupDiskCache(prefs.cacheSize().get().toLong())
init {
@@ -73,7 +71,7 @@ class ChapterCache(private val context: Context) {
/**
* Returns directory of cache.
*/
val cacheDir: File
private val cacheDir: File
get() = diskCache.directory
/**
@@ -100,43 +98,19 @@ class ChapterCache(private val context: Context) {
}
// <-- 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.
*
* @param chapter the chapter.
* @return an observable of the list of pages.
* @return the list of pages.
*/
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
return Observable.fromCallable {
// Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
fun getPageListFromCache(chapter: Chapter): List<Page> {
// Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use {
gson.fromJson<List<Page>>(it.getString(0))
}
// Convert JSON string to list of objects. Throws an exception if snapshot is null
return diskCache.get(key).use {
json.decodeFromString(it.getString(0))
}
}
@@ -148,7 +122,7 @@ class ChapterCache(private val context: Context) {
*/
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string.
val cachedValue = gson.toJson(pages)
val cachedValue = json.encodeToString(pages)
// Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
@@ -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 {
return "${chapter.manga_id}${chapter.url}"
}
@@ -263,7 +263,7 @@ class DownloadManager(private val context: Context) {
if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(manga, source)
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
cleaned += 1 + mangaFolder.listFiles().orEmpty().size
mangaFolder.delete()
cache.removeManga(manga)
return cleaned
@@ -284,8 +284,7 @@ class DownloadManager(private val context: Context) {
if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(manga, source)
val size = mangaFolder.listFiles()?.size ?: 0
if (size == 0) {
if (!mangaFolder.listFiles().isNullOrEmpty()) {
mangaFolder.delete()
cache.removeManga(manga)
} else {
@@ -28,7 +28,6 @@ internal class DownloadNotifier(private val context: Context) {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
}
}
@@ -84,7 +83,6 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onProgressChange(download: Download) {
with(progressNotificationBuilder) {
// Check if first call.
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
clearActions()
@@ -116,6 +114,7 @@ internal class DownloadNotifier(private val context: Context) {
}
setProgress(download.pages!!.size, download.downloadedImages, false)
setOngoing(true)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
@@ -130,6 +129,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp)
setProgress(0, 0, false)
setOngoing(false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source))
return downloadsDir.findFile(getSourceDirName(source), true)
}
/**
@@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
*/
fun findMangaDir(manga: Manga, source: Source): UniFile? {
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? {
val mangaDir = findMangaDir(manga, source)
return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) }
.firstOrNull()
}
@@ -123,14 +123,12 @@ class DownloadProvider(private val context: Context) {
source: Source
): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return mangaDir.listFiles()!!.asList().filter {
(
chapters.find { chp ->
getValidChapterDirNames(chp).any { dir ->
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
}
} == null
) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
return mangaDir.listFiles().orEmpty().asList().filter {
chapters.find { chp ->
getValidChapterDirNames(chp).any { dir ->
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
}
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
}
}
// SY <--
@@ -141,7 +139,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query.
*/
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(
getChapterDirName(chapter),
// TODO: remove this
// Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name)
)
@@ -24,6 +24,7 @@ object PreferenceValues {
enum class DarkThemeVariant {
default,
blue,
amoledblue,
amoled,
red,
midnightdusk,
@@ -11,9 +11,6 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
private val json: Json by injectLazy()
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 {
val originalRequest = chain.request()
@@ -24,21 +21,19 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
if (oauth == null) {
oauth = myanimelist.loadOAuth()
}
// Refresh access token if null or expired.
if (oauth!!.isExpired()) {
// Refresh access token if expired
if (oauth != null && oauth!!.isExpired()) {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
if (it.isSuccessful) {
setAuth(json.decodeFromString(it.body!!.string()))
}
}
}
// Throw on null auth.
if (oauth == null) {
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()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build()
@@ -7,8 +7,9 @@ data class OAuth(
val refresh_token: String,
val access_token: String,
val token_type: String,
val created_at: Long = System.currentTimeMillis(),
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
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
@@ -11,52 +8,26 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
return runBlocking {
try {
val result = GithubUpdateChecker().checkForUpdate()
override fun doWork() = runBlocking {
try {
val result = GithubUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) {
val url = result.release.downloadLink
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
)
}
}
Result.success()
} catch (e: Exception) {
Result.failure()
if (result is UpdateResult.NewUpdate<*>) {
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
}
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object {
private const val TAG = "UpdateChecker"
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
@@ -28,6 +30,27 @@ internal class UpdaterNotifier(private val context: Context) {
context.notificationManager.notify(id, build())
}
fun promptUpdate(url: String) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentIntent(pendingIntent)
clearActions()
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
pendingIntent
)
}
notificationBuilder.show()
}
/**
* Call when apk download starts.
*
@@ -63,19 +86,20 @@ internal class UpdaterNotifier(private val context: Context) {
* @param uri path location of apk.
*/
fun onDownloadFinished(uri: Uri) {
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
with(notificationBuilder) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
setContentIntent(installIntent)
clearActions()
addAction(
R.drawable.ic_system_update_alt_white_24dp,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, uri)
installIntent
)
// Cancel action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),
@@ -96,13 +120,13 @@ internal class UpdaterNotifier(private val context: Context) {
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
clearActions()
addAction(
R.drawable.ic_refresh_24dp,
context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url)
)
// Cancel action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Timber.w(e, "Extension load error: $extName ($it)")
Timber.e(e, "Extension load error: $extName ($it)")
return LoadResult.Error(e)
}
}
@@ -1,17 +1,25 @@
package eu.kanade.tachiyomi.source.online
import android.app.Activity
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
interface LoginSource : Source {
val needsLogin: Boolean
val requiresLogin: Boolean
val twoFactorAuth: AuthSupport
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
enum class AuthSupport {
NOT_SUPPORTED,
SUPPORTED,
REQUIRED
}
}
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
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.asObservableSuccess
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.MangasPage
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.UrlImportableSource
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.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
@@ -46,7 +43,6 @@ import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
@@ -180,21 +176,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
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 {
return MangadexLoginDialog(source as MangaDex)
}
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED
override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrl()
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(
username: String,
password: String,
twoFactorCode: String
twoFactorCode: String?
): Boolean {
return withIOContext {
val formBody = FormBody.Builder().apply {
@@ -202,7 +204,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
add("login_password", password)
add("no_js", "1")
add("remember_me", "1")
add("two_factor", twoFactorCode)
add("two_factor", twoFactorCode ?: "")
}
runCatching {
@@ -223,6 +225,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} else {
throw Exception("Json data was null")
}
}.also {
preferences.setTrackCredentials(trackManager.mdList, username, password)
}
}
}
@@ -1,68 +1,43 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.content.res.Configuration
import android.os.Build
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy()
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?) {
setTheme(
when {
isDarkMode -> darkTheme
else -> lightTheme
val isDarkMode = when (preferences.themeMode().get()) {
ThemeMode.light -> false
ThemeMode.dark -> true
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
val themeId = if (isDarkMode) {
when (preferences.themeDark().get()) {
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
DarkThemeVariant.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)
}
}
@@ -19,7 +19,8 @@ import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
RestoreViewOnCreateController(bundle) {
lateinit var binding: VB
protected lateinit var binding: VB
private set
lateinit var viewScope: CoroutineScope
@@ -51,11 +52,12 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
return inflateView(inflater, container)
}
abstract fun createBinding(inflater: LayoutInflater): VB
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
binding = createBinding(inflater)
return binding.root
}
open fun onViewCreated(view: View) {}
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
@@ -51,10 +50,7 @@ class BrowseController :
return resources!!.getString(R.string.browse)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PagerControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler
@@ -57,18 +56,16 @@ open class ExtensionController :
return ExtensionPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ExtensionControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.swipeRefresh.isRefreshing = true
binding.swipeRefresh.refreshes()
@@ -4,12 +4,12 @@ import android.annotation.SuppressLint
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
private val binding = SectionHeaderItemBinding.bind(view)
@SuppressLint("SetTextI18n")
fun bind(item: ExtensionGroupItem) {
@@ -19,7 +19,7 @@ data class ExtensionGroupItem(val name: String, val size: Int, val showSize: Boo
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header
return R.layout.section_header_item
}
/**
@@ -12,7 +12,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf
import androidx.preference.Preference
@@ -65,15 +64,9 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
setHasOptionsMenu(true)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
return ExtensionDetailControllerBinding.inflate(themedInflater)
}
override fun createPresenter(): ExtensionDetailsPresenter {
@@ -88,6 +81,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
val extension = presenter.extension ?: return
val context = view.context
@@ -6,7 +6,6 @@ import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf
import androidx.preference.DialogPreference
@@ -46,10 +45,9 @@ class SourcePreferencesController(bundle: Bundle? = null) :
bundleOf(SOURCE_ID to sourceId)
)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
override fun createBinding(inflater: LayoutInflater): SourcePreferencesControllerBinding {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = SourcePreferencesControllerBinding.inflate(themedInflater)
return binding.root
return SourcePreferencesControllerBinding.inflate(themedInflater)
}
override fun createPresenter(): SourcePreferencesPresenter {
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.latest
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
@@ -32,23 +31,6 @@ open class LatestController :
*/
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? {
return applicationContext?.getString(R.string.latest)
}
@@ -82,6 +64,8 @@ open class LatestController :
onMangaClick(manga)
}
override fun createBinding(inflater: LayoutInflater): LatestControllerBinding = LatestControllerBinding.inflate(inflater)
/**
* Called when the view is created
*
@@ -90,6 +74,12 @@ open class LatestController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = LatestAdapter(this)
// Create recycler and set adapter.
@@ -6,7 +6,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
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 inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PreMigrationControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = PreMigrationControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
val ourAdapter = adapter ?: MigrationSourceAdapter(
getEnabledSources().map { MigrationSourceItem(it, isEnabled(it.id.toString())) },
@@ -8,7 +8,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.graphics.ColorUtils
import androidx.core.os.bundleOf
@@ -84,24 +83,23 @@ class MigrationListController(bundle: Bundle? = null) :
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 {
return resources?.getString(R.string.migration) + " (${adapter?.items?.count {
it.manga.migrationStatus != MigrationStatus.RUNNING
}}/${adapter?.itemCount ?: 0})"
}
override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
setTitle()
val config = this.config ?: return
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter
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.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -102,16 +103,30 @@ class MigrationProcessAdapter(
// Update chapters read
if (MigrationFlags.hasChapters(flags)) {
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read }.maxByOrNull { it.chapter_number }?.chapter_number
if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
val maxChapterRead =
prevMangaChapters.filter { it.read }.maxOfOrNull { it.chapter_number }
val dbChapters = db.getChapters(manga).executeAsBlocking()
val prevHistoryList = db.getHistoryByMangaId(prevManga.id!!).executeAsBlocking()
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
}
}
db.insertChapters(dbChapters).executeAsBlocking()
}
db.insertChapters(dbChapters).executeAsBlocking()
db.updateHistoryLastRead(historyList).executeAsBlocking()
}
// Update categories
if (MigrationFlags.hasCategories(flags)) {
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
@@ -52,18 +51,16 @@ class MigrationMangaController :
return MigrationMangaPresenter(sourceId)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationMangaControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = MigrationMangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = MigrationMangaAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -42,18 +41,16 @@ class MigrationSourcesController :
return MigrationSourcesPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceAdapter(this)
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.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
/**
* Item that contains the selection header.
@@ -18,7 +18,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header
return R.layout.section_header_item
}
/**
@@ -46,7 +46,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
}
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 {
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 eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
import eu.kanade.tachiyomi.util.system.LocaleHelper
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
private val binding = SectionHeaderItemBinding.bind(view)
fun bind(item: LangItem) {
binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context)
@@ -18,7 +18,7 @@ data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header
return R.layout.section_header_item
}
/**
@@ -9,7 +9,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
@@ -88,25 +87,16 @@ class SourceController(bundle: Bundle? = null) :
return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */)
}
/**
* Initiate the view with [R.layout.source_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceAdapter(this)
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.setVectorCompat
class SourceHolder(private val view: View, val adapter: SourceAdapter /* 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
if (item.isPinned) {
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, view.context.getResourceColor(R.attr.colorAccent))
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, R.attr.colorAccent)
} else {
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, view.context.getResourceColor(android.R.attr.textColorHint))
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, android.R.attr.textColorHint)
}
}
}
@@ -61,6 +61,7 @@ import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch
import exh.source.getMainSource
import exh.source.isEhBasedSource
import exh.widget.preference.MangadexLoginDialog
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
@@ -158,10 +159,7 @@ open class BrowseSourceController(bundle: Bundle) :
// SY <--
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
@@ -182,8 +180,8 @@ open class BrowseSourceController(bundle: Bundle) :
preferences.shownMangaDexSimilarAskDialog().set(true)
}
if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) {
val dialog = mainSource.getLoginDialog(mainSource, activity!!)
if (mainSource is LoginSource && mainSource.requiresLogin && !mainSource.isLogged()) {
val dialog = MangadexLoginDialog(mainSource)
dialog.showDialog(router)
}
// SY <--
@@ -411,6 +409,7 @@ open class BrowseSourceController(bundle: Bundle) :
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
router.popController(this)
} else {
nonSubmittedQuery = ""
searchWithQuery("")
}
@@ -9,6 +9,7 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
@@ -37,10 +38,17 @@ class SourceFilterSheet(
// EXH <--
) : 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 {
filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches, source = source, controller = controller/* SY <-- */)
filterNavView.onFilterClicked = {
onFilterClicked()
this.dismiss()
@@ -56,6 +64,13 @@ class SourceFilterSheet(
// EXH <--
setContentView(filterNavView)
sheetBehavior = BottomSheetBehavior.from(filterNavView.parent as ViewGroup)
}
override fun show() {
super.show()
sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
fun setFilters(items: List<IFlexible<*>>) {
@@ -72,7 +87,15 @@ class SourceFilterSheet(
}
// 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) {
var onFilterClicked = {}
@@ -91,8 +114,13 @@ class SourceFilterSheet(
// SY <--
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 {
// SY -->
@@ -6,7 +6,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
@@ -51,22 +50,7 @@ open class GlobalSearchController(
setHasOptionsMenu(true)
}
/**
* Initiate the view with [R.layout.global_search_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = GlobalSearchControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = GlobalSearchControllerBinding.inflate(inflater)
override fun getTitle(): String? {
return presenter.query
@@ -143,6 +127,12 @@ open class GlobalSearchController(
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = GlobalSearchAdapter(this)
// Create recycler and set adapter.
@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
@@ -74,18 +73,6 @@ open class IndexController :
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? {
return source!!.name
}
@@ -139,6 +126,8 @@ open class IndexController :
}
}
override fun createBinding(inflater: LayoutInflater) = IndexControllerBinding.inflate(inflater)
/**
* Called when the view is created
*
@@ -4,7 +4,6 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
@@ -68,21 +67,7 @@ class CategoryController :
return resources?.getString(R.string.action_edit_categories)
}
/**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
/**
* Called after view inflation. Used to initialize the view.
@@ -92,6 +77,12 @@ class CategoryController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = CategoryAdapter(this@CategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
@@ -4,7 +4,6 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
@@ -68,21 +67,7 @@ class BiometricTimesController :
return resources?.getString(R.string.biometric_lock_times)
}
/**
* 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
}
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
/**
* Called after view inflation. Used to initialize the view.
@@ -92,6 +77,12 @@ class BiometricTimesController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = BiometricTimesAdapter(this@BiometricTimesController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
@@ -74,21 +73,7 @@ class SortTagController :
setHasOptionsMenu(true)
}
/**
* 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
}
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
/**
* Called after view inflation. Used to initialize the view.
@@ -98,6 +83,12 @@ class SortTagController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SortTagAdapter(this@SortTagController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
@@ -4,7 +4,6 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
@@ -65,21 +64,7 @@ class RepoController :
return resources?.getString(R.string.action_edit_repos)
}
/**
* 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
}
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
/**
* Called after view inflation. Used to initialize the view.
@@ -89,6 +74,12 @@ class RepoController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = RepoAdapter(this@RepoController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
@@ -4,7 +4,6 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
@@ -66,21 +65,7 @@ class SourceCategoryController :
return resources?.getString(R.string.action_edit_categories)
}
/**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
/**
* Called after view inflation. Used to initialize the view.
@@ -90,6 +75,12 @@ class SourceCategoryController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SourceCategoryAdapter(this@SourceCategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -55,15 +54,7 @@ class DownloadController :
setHasOptionsMenu(true)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = DownloadControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = DownloadControllerBinding.inflate(inflater)
override fun createPresenter(): DownloadPresenter {
return DownloadPresenter()
@@ -76,6 +67,12 @@ class DownloadController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Check if download queue is empty and update information accordingly.
setInformationView()
@@ -81,15 +81,14 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
private fun showPopupMenu(view: View) {
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_bottom).isVisible =
bindingAdapterPosition != adapter.itemCount - 1
},
{
onMenuItemClick = {
adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this)
true
}
)
}
@@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
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.PublishRelay
import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
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.withFadeTransaction
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.main.MainActivity
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 exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus
import exh.source.MERGED_SOURCE_ID
import exh.source.PERV_EDEN_EN_SOURCE_ID
import exh.source.PERV_EDEN_IT_SOURCE_ID
import exh.source.isEhBasedManga
@@ -193,14 +193,17 @@ class LibraryController(
return LibraryPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = LibraryControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
adapter = LibraryAdapter(this)
binding.libraryPager.adapter = adapter
binding.libraryPager.pageSelections()
@@ -473,9 +476,6 @@ class LibraryController(
}
}
// SY -->
R.id.action_source_migration -> {
router.pushController(MigrationSourcesController().withFadeTransaction())
}
R.id.action_sync_favorites -> {
if (preferences.exhShowSyncIntro().get()) {
activity?.let { FavoritesIntroDialog().show(it) }
@@ -542,9 +542,13 @@ class LibraryController(
// SY -->
R.id.action_migrate -> {
val skipPre = preferences.skipPreMigration().get()
val selectedMangaIds = selectedMangas.mapNotNull { it.id }
val selectedMangaIds = selectedMangas.filterNot { it.source == MERGED_SOURCE_ID }.mapNotNull { it.id }
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_push_to_mdlist -> pushToMdList()
@@ -14,6 +14,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceDialogController
@@ -48,6 +49,8 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.InternalResourceHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker
@@ -123,23 +126,27 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
padding(left = true, top = true, right = true)
}
}
binding.bottomNav.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.rootFab.applyInsetter {
type(navigationBars = true) {
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 ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
// Keep scrim on light theme if windowLightNavigationBar is not available
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 || isDarkMode) {
window.navigationBarColor = Color.TRANSPARENT
window.navigationBarColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
!InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true)
) {
Color.TRANSPARENT
} else {
// Set navbar scrim 70% of navigationBarColor
getResourceColor(android.R.attr.navigationBarColor, 0.7F)
}
}
insets
@@ -148,6 +155,15 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
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
preferences.hideBottomBar()
.asImmediateFlow { setBottomNavBehaviorOnScroll() }
@@ -171,14 +187,9 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
val controller = router.getControllerWithTag(id.toString()) as? LibraryController
controller?.showSettingsSheet()
}
// SY -->
R.id.nav_updates -> {
if (router.backstack.lastOrNull()?.controller() !is DownloadController) {
val controller = router.getControllerWithTag(id.toString()) as? UpdatesController
controller?.router?.pushController(DownloadController().withFadeTransaction())
}
router.pushController(DownloadController().withFadeTransaction())
}
// SY <--
}
}
true
@@ -500,7 +511,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
fun fixViewToBottom(view: View) {
val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val maxAbsOffset = appBarLayout.measuredHeight - binding.tabs.measuredHeight
view.translationY = -maxAbsOffset - verticalOffset.toFloat()
view.translationY = -maxAbsOffset - verticalOffset.toFloat() + appBarLayout.marginTop
}
binding.appbar.addOnOffsetChangedListener(listener)
fixedViewsToBottom[view] = listener
@@ -218,8 +218,8 @@ class EditMangaDialog : DialogController {
private fun ChipGroup.setChips(items: List<String>) {
removeAllViews()
items.forEach { item ->
val chip = Chip(context).apply {
items.asSequence().map { item ->
Chip(context).apply {
text = item
isCloseIconVisible = true
@@ -228,15 +228,16 @@ class EditMangaDialog : DialogController {
removeView(this)
}
}
addView(chip)
}.forEach {
addView(it)
}
val addTagChip = Chip(context).apply {
setText(R.string.add_tag)
chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)
chipIcon?.setTint(context.getResourceColor(R.attr.colorAccent))
chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_24dp)?.apply {
setTint(context.getResourceColor(R.attr.colorAccent))
}
textStartPadding = 0F
clicks().onEach {
@@ -17,7 +17,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
@@ -276,18 +275,21 @@ class MangaController :
)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MangaControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = MangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
if (manga == null || source == null) return
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
@@ -479,7 +481,7 @@ class MangaController :
// 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_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 -->
menu.findItem(R.id.action_edit).isVisible = presenter.manga.favorite || isLocalSource
@@ -1145,8 +1147,7 @@ class MangaController :
fun onChapterDownloadUpdate(download: Download) {
chaptersAdapter?.currentItems?.find { it.id == download.chapter.id }?.let {
chaptersAdapter?.updateItem(it)
chaptersAdapter?.notifyDataSetChanged()
chaptersAdapter?.updateItem(it, it.status)
}
}
@@ -1270,7 +1271,6 @@ class MangaController :
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
// Hide FAB to avoid interfering with the bottom action toolbar
// actionFab?.hide()
actionFab?.isVisible = false
}
return false
@@ -1302,10 +1302,6 @@ class MangaController :
chaptersAdapter?.clearSelection()
selectedChapters.clear()
actionMode = null
// TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton]
// fails to show up properly
// actionFab?.show()
actionFab?.isVisible = true
}
@@ -1427,14 +1423,26 @@ class MangaController :
// OVERFLOW MENU DIALOGS
private fun getUnreadChaptersSorted() = /* SY --> */ if (presenter.source.isEhBasedSource()) presenter.chapters
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.distinctBy { it.name }
.sortedBy { it.source_order }
else /* SY <-- */ presenter.chapters
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.distinctBy { it.name }
.sortedByDescending { it.source_order }
private fun getUnreadChaptersSorted(): List<ChapterItem> {
val chapters = presenter.chapters
.sortedWith(presenter.getChapterSort())
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.distinctBy { it.name }
// SY -->
.let {
if (presenter.source.isEhBasedSource()) {
it.reversed()
} else {
it
}
}
// SY <--
return if (presenter.sortDescending()) {
chapters.reversed()
} else {
chapters
}
}
private fun downloadChapters(choice: Int) {
val chaptersToDownload = when (choice) {
@@ -846,7 +846,11 @@ class MangaPresenter(
}
// 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()) {
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
@@ -861,8 +865,6 @@ class MangaPresenter(
}
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.
*/
fun getNextUnreadChapter(): ChapterItem? {
val chapters = chapters.sortedWith(getChapterSort())
return if (source.isEhBasedSource()) {
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
if (chapter?.read == false) chapter else null
if (sortDescending()) {
chapters.firstOrNull()?.takeUnless { it.read }
} else {
chapters.lastOrNull()?.takeUnless { it.read }
}
} 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.widget.FrameLayout
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.ChapterDownloadViewBinding
import eu.kanade.tachiyomi.util.view.setVectorCompat
class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
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 progress = 0
private var downloadIconAnimator: ObjectAnimator? = null
private var isAnimating = false
init {
binding = ChapterDownloadViewBinding.inflate(LayoutInflater.from(context), this, false)
addView(binding.root)
}
fun setState(state: Download.State, progress: Int = 0) {
val isDirty = this.state.value != state.value || this.progress != progress
this.state = state
this.progress = progress
if (isDirty) {
updateLayout()
updateLayout(state, progress)
}
}
private fun updateLayout() {
binding.downloadIconBorder.isVisible = state == Download.State.NOT_DOWNLOADED
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || state == Download.State.DOWNLOADING
if (state == Download.State.DOWNLOADING) {
if (!isAnimating) {
private fun updateLayout(state: Download.State, progress: Int) {
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED ||
state == Download.State.DOWNLOADING || state == Download.State.QUEUE
if (state == Download.State.DOWNLOADING || state == Download.State.QUEUE) {
if (downloadIconAnimator == null) {
downloadIconAnimator =
ObjectAnimator.ofFloat(binding.downloadIcon, "alpha", 1f, 0f).apply {
duration = 1000
@@ -49,22 +45,36 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
repeatMode = ObjectAnimator.REVERSE
}
downloadIconAnimator?.start()
isAnimating = true
}
} else {
downloadIconAnimator?.currentPlayTime = System.currentTimeMillis() % 2000
} else if (downloadIconAnimator != null) {
downloadIconAnimator?.cancel()
downloadIconAnimator = null
binding.downloadIcon.alpha = 1f
isAnimating = false
}
binding.downloadQueued.isVisible = state == Download.State.QUEUE
binding.downloadProgress.isVisible = state == Download.State.DOWNLOADING ||
(state == Download.State.QUEUE && progress > 0)
binding.downloadProgress.progress = progress
state == Download.State.NOT_DOWNLOADED || state == Download.State.QUEUE
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
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
@@ -64,8 +64,10 @@ class ChapterHolder(
} else /* SY <-- */ descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload)))
}
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 {
setSpan(ForegroundColorSpan(adapter.readColor), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
val lastPageRead = buildSpannedString {
color(adapter.readColor) {
append(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1))
}
}
descriptions.add(lastPageRead)
}
@@ -61,16 +61,12 @@ class ChaptersSettingsSheet(
private fun showPopupMenu(view: View) {
view.popupMenu(
R.menu.default_chapter_filter,
{
},
{
when (this.itemId) {
menuRes = R.menu.default_chapter_filter,
onMenuItemClick = {
when (itemId) {
R.id.set_as_default -> {
SetChapterSettingsDialog(presenter.manga).showDialog(router)
true
}
else -> true
}
}
)
@@ -29,7 +29,6 @@ open class BaseChapterHolder(
},
onMenuItemClick = {
adapter.clickListener.deleteChapter(position)
true
}
)
}
@@ -42,7 +42,7 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
android.R.layout.simple_spinner_item,
listOf(
"No dedupe",
"Dedupe by priority",
/*"Dedupe by priority",*/
"Show source with most chapters",
"Show source with highest chapter number"
)
@@ -53,9 +53,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
binding.dedupeModeSpinner.setSelection(
when (it.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0
MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 2
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 3
/*MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1*/
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 1
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 2
else -> 0
}
)
@@ -69,9 +69,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
) {
controller.mergeReference?.chapterSortMode = when (position) {
0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY
2 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
3 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
/*1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY*/
1 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
2 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
}
xLogD(controller.mergeReference?.chapterSortMode)
@@ -85,7 +85,13 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
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)
binding.mangaInfoSpinner.adapter = mangaInfoAdapter
@@ -102,11 +108,16 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
position: Int,
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 {
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.main.WhatsNewDialogController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.preference.onClick
@@ -87,6 +88,14 @@ class AboutController : SettingsController() {
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_facebook"
title = "Facebook"
"https://facebook.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_twitter"
title = "Twitter"
@@ -117,7 +126,7 @@ class AboutController : SettingsController() {
preference {
key = "pref_about_label_original_tachiyomi_github"
title = "Original Tachiyomi GitHub "
"https://github.com/tachiyomiorg/tachiyomi".also {
"https://github.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
@@ -140,6 +149,7 @@ class AboutController : SettingsController() {
.withAboutIconShown(false)
.withAboutVersionShown(false)
.withLicenseShown(true)
.withEdgeToEdge(true)
.start(activity!!)
}
}
@@ -152,6 +162,11 @@ class AboutController : SettingsController() {
private fun checkVersion() {
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)
launchNow {
@@ -203,20 +218,10 @@ class AboutController : SettingsController() {
}
private fun copyDebugInfo() {
val deviceInfo =
"""
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()
activity?.copyToClipboard("Debug information", deviceInfo)
activity?.let {
val deviceInfo = CrashLogUtil(it).getDebugInfo()
activity?.copyToClipboard("Debug information", deviceInfo)
}
}
private fun getFormattedBuildTime(): String {
@@ -1,7 +1,11 @@
package eu.kanade.tachiyomi.ui.more
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
@@ -27,7 +31,10 @@ import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
import exh.ui.batchadd.BatchAddController
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@@ -40,6 +47,9 @@ class MoreController :
private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0
private var untilDestroySubscriptions = CompositeSubscription()
private set
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_more
@@ -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) {
// Handle running/paused status change
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) :
Preference(context, attrs) {
@@ -11,7 +11,6 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent
import android.view.Menu
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.hideBar
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.showBar
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
@@ -458,13 +458,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setTooltip(R.string.viewer)
setOnClickListener {
val newReadingMode =
ReadingModeType.getNextReadingMode(presenter.getMangaViewer(resolveDefault = false))
presenter.setMangaViewer(newReadingMode.prefValue)
popupMenu(
items = ReadingModeType.values().map { it.prefValue to it.stringRes },
selectedItemId = presenter.getMangaViewer(resolveDefault = false),
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
menuToggleToast?.cancel()
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes)
presenter.setMangaViewer(newReadingMode.prefValue)
menuToggleToast?.cancel()
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
}
}
}
@@ -474,14 +479,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setTooltip(R.string.pref_rotation_type)
setOnClickListener {
val newOrientation =
OrientationType.getNextOrientation(preferences.rotation().get(), resources)
popupMenu(
items = OrientationType.values().map { it.prefValue to it.stringRes },
selectedItemId = preferences.rotation().get(),
) {
val newOrientation = OrientationType.fromPreference(itemId)
preferences.rotation().set(newOrientation.prefValue)
setOrientation(newOrientation.flag)
preferences.rotation().set(newOrientation.prefValue)
setOrientation(newOrientation.flag)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
}
}
}
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
@@ -523,6 +532,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
setOnClickListener {
ReaderSettingsSheet(this@ReaderActivity).show()
}
setOnLongClickListener {
ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).show()
true
}
}
// --> EH
@@ -718,7 +732,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// EXH <--
/*private fun updateRotationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference, resources)
val orientation = OrientationType.fromPreference(preference)
binding.actionRotation.setImageResource(orientation.iconRes)
}*/
@@ -885,12 +899,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView())
// 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) {
readingModeToast?.cancel()
readingModeToast = this.toast(resources.getString(R.string.eh_auto_webtoon_snack)) {
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
}
readingModeToast = toast(resources.getString(R.string.eh_auto_webtoon_snack))
} else if (preferences.showReadingMode()) {
// SY <--
showReadingModeToast(presenter.getMangaViewer())
@@ -946,10 +958,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
}
private fun showReadingModeToast(mode: Int) {
val strings = resources.getStringArray(R.array.viewers_selector)
readingModeToast?.cancel()
readingModeToast = toast(strings[mode]) {
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
try {
val strings = resources.getStringArray(R.array.viewers_selector)
readingModeToast?.cancel()
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.
*/
private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation, resources)
val newOrientation = OrientationType.fromPreference(orientation)
if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag
}
@@ -598,7 +598,7 @@ class ReaderPresenter(
val manga = manga ?: return preferences.defaultViewer()
// SY -->
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) {
preferences.defaultViewer()
} else {
@@ -99,8 +99,7 @@ class HttpPageLoader(
* the local cache, otherwise fallbacks to network.
*/
override fun getPages(): Observable<List<ReaderPage>> {
return chapterCache
.getPageListFromCache(chapter.chapter)
return Observable.fromCallable { chapterCache.getPageListFromCache(chapter.chapter) }
.onErrorResumeNext { source.fetchPageList(chapter.chapter) }
.map { pages ->
// SY -->
@@ -1,43 +1,20 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.next
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp),
LOCKED_PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp),
LOCKED_LANDSCAPE(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp),
PORTRAIT(3, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp),
LANDSCAPE(4, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp);
PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp),
LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp),
LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp),
LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp),
;
companion object {
fun fromPreference(preference: Int, resources: Resources): OrientationType = when (preference) {
2 -> {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
LOCKED_PORTRAIT
} else {
LOCKED_LANDSCAPE
}
}
3 -> PORTRAIT
4 -> LANDSCAPE
else -> FREE
}
fun getNextOrientation(preference: Int, resources: Resources): OrientationType {
val current = if (preference == 2) {
// Avoid issue due to 2 types having the same prefValue
LOCKED_LANDSCAPE
} else {
fromPreference(preference, resources)
}
return current.next()
}
fun fromPreference(preference: Int): OrientationType =
values().find { it.prefValue == preference } ?: FREE
}
}
@@ -8,7 +8,10 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.widget.SimpleTabSelectedListener
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 generalSettings = ReaderGeneralSettings(activity)
@@ -19,7 +22,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : TabbedBottomSh
init {
val sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup)
sheetBehavior.isFitToContents = false
sheetBehavior.halfExpandedRatio = 0.5f
sheetBehavior.halfExpandedRatio = 0.25f
val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() {
@@ -33,13 +36,12 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : TabbedBottomSh
if (activity.menuVisible != !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(
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
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) {
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 {
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 {
val mode = fromPreference(preference)
return mode == LEFT_TO_RIGHT || mode == RIGHT_TO_LEFT || mode == VERTICAL
@@ -250,16 +250,13 @@ class PagerPageHolder(
readImageHeaderSubscription = Observable
.fromCallable {
val stream = streamFn().buffered(16)
openStream = stream
openStream = process(stream)
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
openStream = processDualPageSplit(openStream!!)
}
if (!isAnimated) {
// SY -->
if (readerTheme >= 3) {
@@ -295,21 +292,31 @@ class PagerPageHolder(
.subscribe({}, {})
}
private fun processDualPageSplit(openStream: InputStream): InputStream {
var inputStream = openStream
val (isDoublePage, stream) = when (page) {
is InsertPage -> Pair(true, inputStream)
else -> ImageUtil.isDoublePage(inputStream)
private fun process(imageStream: InputStream): InputStream {
if (!viewer.config.dualPageSplit) {
return imageStream
}
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 {
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 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!")
}
@@ -320,11 +327,7 @@ class PagerPageHolder(
}
}
if (page !is InsertPage) {
onPageSplit()
}
return ImageUtil.splitInHalf(inputStream, side)
return ImageUtil.splitInHalf(imageStream, side)
}
private fun onPageSplit() {
@@ -385,7 +385,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
}
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() {
@@ -281,22 +281,13 @@ class WebtoonPageHolder(
readImageHeaderSubscription = Observable
.fromCallable {
val stream = streamFn().buffered(16)
openStream = stream
openStream = process(stream)
ImageUtil.findImageType(stream) == ImageUtil.ImageType.GIF
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.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) {
val subsamplingView = initSubsamplingImageView()
subsamplingView.isVisible = true
@@ -315,6 +306,20 @@ class WebtoonPageHolder(
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.
*/
@@ -79,16 +79,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
recycler.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val position = 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)
}
}
onScrolled()
if (dy < 0) {
val firstIndex = layoutManager.findFirstVisibleItemPosition()
@@ -249,11 +240,27 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
val position = adapter.items.indexOf(page)
if (position != -1) {
recycler.scrollToPosition(position)
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
onScrolled(position)
}
} else {
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].
*/
@@ -8,13 +8,13 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.RecentSectionItemBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
import java.util.Date
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
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 {
@@ -39,12 +39,12 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateS
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
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 {
setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
}
interface OnResumeClickListener {
@@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
@@ -20,7 +19,6 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
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.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@@ -36,13 +34,10 @@ import uy.kohesive.injekt.injectLazy
/**
* Fragment that shows recently read manga.
* Uses [R.layout.history_controller].
* UI related actions should be called from here.
*/
class HistoryController :
NucleusController<HistoryControllerBinding, HistoryPresenter>(),
RootController,
NoToolbarElevationController,
FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.EndlessScrollListener,
HistoryAdapter.OnRemoveClickListener,
@@ -76,18 +71,16 @@ class HistoryController :
return HistoryPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = HistoryControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
// Initialize adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context)
@@ -18,7 +18,6 @@ class UpdatesAdapter(
init {
setDisplayHeadersAtStartUp(true)
setStickyHeaders(true)
}
interface OnCoverClickListener {
@@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.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.notification.Notifications
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.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@@ -37,13 +35,10 @@ import timber.log.Timber
/**
* Fragment that shows recent chapters.
* Uses [R.layout.updates_controller].
* UI related actions should be called from here.
*/
class UpdatesController :
NucleusController<UpdatesControllerBinding, UpdatesPresenter>(),
RootController,
NoToolbarElevationController,
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
@@ -75,18 +70,21 @@ class UpdatesController :
return UpdatesPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = UpdatesControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) = UpdatesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
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)
// Init RecyclerView and adapter
@@ -244,8 +242,7 @@ class UpdatesController :
adapter?.currentItems
?.filterIsInstance<UpdatesItem>()
?.find { it.chapter.id == download.chapter.id }?.let {
adapter?.updateItem(it)
adapter?.notifyDataSetChanged()
adapter?.updateItem(it, it.status)
}
}
@@ -1,21 +1,19 @@
package eu.kanade.tachiyomi.ui.security
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
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 uy.kohesive.injekt.injectLazy
import timber.log.Timber
import java.util.Date
import java.util.concurrent.Executors
/**
* Blank activity with a BiometricPrompt.
*/
class BiometricUnlockActivity : AppCompatActivity() {
class BiometricUnlockActivity : BaseThemedActivity() {
private val preferences: PreferencesHelper by injectLazy()
private val executor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -27,6 +25,7 @@ class BiometricUnlockActivity : AppCompatActivity() {
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Timber.e(errString.toString())
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.withFadeTransaction
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.editTextPreference
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.switchPreference
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.toast
import exh.debug.SettingsDebugController
@@ -47,15 +49,8 @@ import exh.log.EHLogLevel
import exh.source.BlacklistedSources
import exh.source.EH_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.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.api.get
import uy.kohesive.injekt.injectLazy
@@ -349,31 +344,32 @@ class SettingsAdvancedController : SettingsController() {
private fun cleanupDownloads(removeRead: Boolean, removeNonFavorite: Boolean) {
if (job?.isActive == true) return
activity?.toast(R.string.starting_cleanup)
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
job = launchIO {
val mangaList = db.getMangas().executeAsBlocking()
val sourceManager: SourceManager = Injekt.get()
val downloadManager: DownloadManager = Injekt.get()
var foldersCleared = 0
val sources = sourceManager.getOnlineSources()
for (source in sources) {
Injekt.get<SourceManager>().getOnlineSources().forEach { 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) {
val manga = sourceManga.find { it.originalTitle == mangaFolder.name }
mangaFolders.forEach mangaFolder@{ mangaFolder ->
val manga = sourceManga.find { (_, folderName) -> folderName == mangaFolder.name }?.first
if (manga == null) {
// download is orphaned delete it
foldersCleared += 1 + (mangaFolder.listFiles()?.size ?: 0)
foldersCleared += 1 + (mangaFolder.listFiles().orEmpty().size)
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 {
val activity = activity ?: return@launchUI
withUIContext {
val activity = activity ?: return@withUIContext
val cleanupString =
if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup)
else resources!!.getQuantityString(
@@ -389,27 +385,18 @@ class SettingsAdvancedController : SettingsController() {
private fun clearChapterCache() {
if (activity == null) return
val files = chapterCache.cacheDir.listFiles() ?: return
var deletedFiles = 0
Observable.defer { Observable.from(files) }
.doOnNext { file ->
if (chapterCache.removeFileFromCache(file.name)) {
deletedFiles++
launchIO {
try {
val deletedFiles = chapterCache.clear()
withUIContext {
activity?.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
findPreference(CLEAR_CACHE_KEY)?.summary =
resources?.getString(R.string.used_cache, chapterCache.readableSize)
}
} 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() {
@@ -24,9 +24,6 @@ import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.coroutines.MainScope
import rx.Observable
import rx.Subscription
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -35,19 +32,14 @@ abstract class SettingsController : PreferenceController() {
var preferenceKey: String? = null
val preferences: PreferencesHelper = Injekt.get()
val viewScope = MainScope()
var untilDestroySubscriptions = CompositeSubscription()
private set
private var themedContext: Context? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
val view = super.onCreateView(inflater, container, savedInstanceState)
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 {
@@ -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) {
super.onDestroyView(view)
untilDestroySubscriptions.unsubscribe()
themedContext = null
}
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
setupPreferenceScreen(screen)
}
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) {
ValueAnimator
.ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.rippleColor))
@@ -111,7 +109,7 @@ abstract class SettingsController : PreferenceController() {
return preferenceScreen?.title?.toString()
}
fun setTitle() {
private fun setTitle() {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController<*> && parentController.getTitle() != null) {
@@ -122,16 +120,4 @@ abstract class SettingsController : PreferenceController() {
(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(
R.string.theme_dark_default,
R.string.theme_dark_blue,
R.string.theme_dark_amoledblue,
R.string.theme_dark_amoled,
R.string.theme_dark_red,
R.string.theme_dark_midnightdusk,
@@ -140,6 +141,7 @@ class SettingsGeneralController : SettingsController() {
entryValues = arrayOf(
Values.DarkThemeVariant.default.name,
Values.DarkThemeVariant.blue.name,
Values.DarkThemeVariant.amoledblue.name,
Values.DarkThemeVariant.amoled.name,
Values.DarkThemeVariant.red.name,
Values.DarkThemeVariant.midnightdusk.name,
@@ -93,11 +93,12 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.pref_rotation_type
entriesRes = arrayOf(
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_landscape
R.string.rotation_force_landscape,
)
entryValues = arrayOf("1", "2", "3", "4")
entryValues = arrayOf("1", "2", "3", "4", "5")
defaultValue = "1"
summary = "%s"
}
@@ -6,7 +6,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
@@ -33,17 +32,7 @@ class SettingsSearchController :
setHasOptionsMenu(true)
}
/**
* 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 createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater)
override fun getTitle(): String? {
return presenter.query
@@ -139,8 +139,10 @@ class WebViewActivity : BaseViewBindingActivity<WebviewActivityBinding>() {
}
override fun onDestroy() {
binding.webview?.destroy()
super.onDestroy()
// Binding sometimes isn't actually instantiated yet somehow
binding?.webview?.destroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -2,15 +2,19 @@ package eu.kanade.tachiyomi.util
import android.content.Context
import android.net.Uri
import android.os.Build
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
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.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import java.io.IOException
import exh.syDebugVersion
class CrashLogUtil(private val context: Context) {
@@ -19,31 +23,45 @@ class CrashLogUtil(private val context: Context) {
}
fun dumpLogs() {
try {
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}")
launchIO {
try {
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))
} catch (e: IOException) {
context.toast("Failed to get logs")
showNotification(file.getUriCompat(context))
} catch (e: Throwable) {
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) {
context.notificationManager.cancel(Notifications.ID_CRASH_LOGS)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.crash_log_saved))
// Clear old actions if they exist
clearActions()
addAction(
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)
addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.action_share),
@@ -173,3 +173,5 @@ private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: SChapter):
dbChapter.date_upload != sourceChapter.date_upload ||
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
import android.content.Context
import android.os.Build
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators
object BiometricUtil {
fun getSupportedAuthenticators(context: Context): Int {
if (isLegacySecured(context)) {
return Authenticators.BIOMETRIC_WEAK or Authenticators.DEVICE_CREDENTIAL
}
return listOf(
Authenticators.BIOMETRIC_STRONG,
Authenticators.BIOMETRIC_WEAK,
@@ -17,10 +22,22 @@ object BiometricUtil {
}
fun isSupported(context: Context): Boolean {
return getSupportedAuthenticators(context) != 0
return isLegacySecured(context) || getSupportedAuthenticators(context) != 0
}
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
import android.app.ActivityManager
import android.app.KeyguardManager
import android.app.Notification
import android.app.NotificationManager
import android.content.BroadcastReceiver
@@ -33,6 +34,7 @@ import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.truncateCenter
import timber.log.Timber
import java.io.File
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) {
if (content.isBlank()) return
val clipboard = getSystemService<ClipboardManager>()!!
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
try {
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
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
/**
* Property to get the notification manager from the context.
*/
val Context.notificationManager: NotificationManager
get() = getSystemService()!!
/**
* Property to get the connectivity manager from the context.
*/
val Context.connectivityManager: ConnectivityManager
get() = getSystemService()!!
/**
* Property to get the power manager from the context.
*/
val Context.powerManager: PowerManager
get() = getSystemService()!!
val Context.keyguardManager: KeyguardManager
get() = getSystemService()!!
/**
* 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 options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
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
import android.widget.ImageView
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import eu.kanade.tachiyomi.util.system.getResourceColor
/**
* Set a vector on a [ImageView].
*
* @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)
if (tint != null) {
vector?.mutate()
vector?.setTint(tint)
vector?.setTint(context.getResourceColor(tint))
}
setImageDrawable(vector)
}
@@ -2,6 +2,7 @@
package eu.kanade.tachiyomi.util.view
import android.annotation.SuppressLint
import android.graphics.Point
import android.view.Gravity
import android.view.Menu
@@ -9,14 +10,18 @@ import android.view.MenuItem
import android.view.View
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.view.forEach
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
/**
* Returns coordinates of view.
@@ -63,7 +68,7 @@ inline fun View.setTooltip(@StringRes stringRes: Int) {
inline fun View.popupMenu(
@MenuRes menuRes: Int,
noinline initMenu: (Menu.() -> Unit)? = null,
noinline onMenuItemClick: MenuItem.() -> Boolean
noinline onMenuItemClick: MenuItem.() -> Unit
): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
popup.menuInflater.inflate(menuRes, popup.menu)
@@ -71,7 +76,50 @@ inline fun View.popupMenu(
if (initMenu != null) {
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()
return popup
@@ -12,7 +12,7 @@ import androidx.annotation.MenuRes
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CommonActionToolbarBinding
import eu.kanade.tachiyomi.databinding.ActionToolbarBinding
/**
* 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) :
FrameLayout(context, attrs) {
private val binding: CommonActionToolbarBinding
init {
binding = CommonActionToolbarBinding.inflate(LayoutInflater.from(context), this, true)
}
private val binding = ActionToolbarBinding.inflate(LayoutInflater.from(context), this, true)
/**
* Remove menu items and remove listener.
*/
fun destroy() {
binding.commonActionMenu.menu.clear()
binding.commonActionMenu.setOnMenuItemClickListener(null)
binding.menu.menu.clear()
binding.menu.setOnMenuItemClickListener(null)
}
/**
* Gets a menu item if found.
*/
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) {
// Avoid re-inflating the menu
if (binding.commonActionMenu.menu.size() == 0) {
mode.menuInflater.inflate(menuRes, binding.commonActionMenu.menu)
binding.commonActionMenu.setOnMenuItemClickListener { listener(it) }
if (binding.menu.menu.size() == 0) {
mode.menuInflater.inflate(menuRes, binding.menu.menu)
binding.menu.setOnMenuItemClickListener { listener(it) }
}
binding.commonActionToolbar.isVisible = true
binding.actionToolbar.isVisible = true
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(
object : SimpleAnimationListener() {
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.util.AttributeSet
import android.view.Gravity
@@ -7,15 +8,21 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.widget.FrameLayout
import androidx.annotation.ArrayRes
import androidx.appcompat.view.menu.MenuBuilder
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 eu.kanade.tachiyomi.R
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) {
private var entries = emptyList<String>()
private var selectedPosition = 0
private var popup: PopupMenu? = 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)
init {
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
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
binding.details.text = entries.firstOrNull().orEmpty()
@@ -48,6 +64,14 @@ class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: Attri
}
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()
}
@@ -118,11 +142,19 @@ class SpinnerPreference @JvmOverloads constructor(context: Context, attrs: Attri
return pos
}
@SuppressLint("RestrictedApi")
fun createPopupMenu(onItemClick: (Int) -> Unit): PopupMenu {
val popup = PopupMenu(context, this, Gravity.END, R.attr.actionOverflowMenuStyle, 0)
entries.forEachIndexed { index, 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 ->
val pos = menuClicked(menuItem)
onItemClick(pos)
@@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.widget.materialdialogs
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageView
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) :
AppCompatImageView(context, attrs) {
@@ -19,19 +16,11 @@ class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: Attri
}
private fun updateDrawable() {
val drawable = when (state) {
State.UNCHECKED -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
State.INDETERMINATE -> tintVector(context, R.drawable.ic_indeterminate_check_box_24dp)
State.CHECKED -> tintVector(context, R.drawable.ic_check_box_24dp)
State.INVERSED -> tintVector(context, R.drawable.ic_check_box_x_24dp)
}
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))
when (state) {
State.UNCHECKED -> setVectorCompat(R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
State.INDETERMINATE -> setVectorCompat(R.drawable.ic_indeterminate_check_box_24dp, R.attr.colorAccent)
State.CHECKED -> setVectorCompat(R.drawable.ic_check_box_24dp, R.attr.colorAccent)
State.INVERSED -> setVectorCompat(R.drawable.ic_check_box_x_24dp, R.attr.colorAccent)
}
}

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