Compare commits

...

47 Commits

Author SHA1 Message Date
e863e8c64b Adjust Wi-Fi connection check (related to #6038) 2021-10-04 17:06:24 -04:00
f5b591430c Release v0.12.3 2021-10-04 15:55:06 -04:00
8cfaf8eb51 Weblate translations (#5913)
Co-authored-by: AHmed HarBy <themagic1093@gmail.com>
Co-authored-by: Ainārs Lapkovskis <ainarslapkovskis@gmail.com>
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: Alifian Caesar <alifiancaesar@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Blue <bluestuffish@gmail.com>
Co-authored-by: Christian Elbrianno <christian.elbrianno41@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Druvvaldis <druvvaldisr@gmail.com>
Co-authored-by: Emerson Nunes <emerson.nunes.ds@gmail.com>
Co-authored-by: Emma Jane Bonestell <EmmaJaneBonestell@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Fernando Sanchez <cheeze.sprinkels@gmail.com>
Co-authored-by: Forqen <krecio555@gmail.com>
Co-authored-by: Francesco Zanella <franzghosts@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hassay Ádám Tamás <hassayadam@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 <jakubfabijan@tuta.io>
Co-authored-by: Junan Chk <junanchakma2000@gmail.com>
Co-authored-by: K. Sz. Bence <tudi20@protonmail.com>
Co-authored-by: Kaleb <kalebcarvalho1@gmail.com>
Co-authored-by: Krishna Chand <krishna_chand67@naver.com>
Co-authored-by: LoneHash <sameepsk2@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Miguel Alexandro Manzano Guerra <kuro_eis@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nishant Bodkhe <nishantbodkhe44@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Pedro <pedro-mediavilla@hotmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Redy Apriyadi <redy.apriyadi@gmail.com>
Co-authored-by: Ric <rikku.debec@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Sieg Jaeger <zekerett@gmail.com>
Co-authored-by: Steven Pedroza <stevenpedroza56@gmail.com>
Co-authored-by: Temporary Person <TemporaryPerson@protonmail.com>
Co-authored-by: Zero O <godarms2010@live.com>
Co-authored-by: ZiomaleQ <r.partyka30@gmail.com>
Co-authored-by: crackheadakira <lasn.mine@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: julptk <julptk8@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: phannhanhn201 <phannhanhn201@gmail.com>
Co-authored-by: rytis sertvytis <knysliukas2002@gmail.com>
Co-authored-by: soplatnik <jestapom@protonmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: พรหมชัย ชูแสง <promchai2sin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/aii/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/jv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/mr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: AHmed HarBy <themagic1093@gmail.com>
Co-authored-by: Ainārs Lapkovskis <ainarslapkovskis@gmail.com>
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: Alifian Caesar <alifiancaesar@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Blue <bluestuffish@gmail.com>
Co-authored-by: Christian Elbrianno <christian.elbrianno41@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Druvvaldis <druvvaldisr@gmail.com>
Co-authored-by: Emerson Nunes <emerson.nunes.ds@gmail.com>
Co-authored-by: Emma Jane Bonestell <EmmaJaneBonestell@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Fernando Sanchez <cheeze.sprinkels@gmail.com>
Co-authored-by: Forqen <krecio555@gmail.com>
Co-authored-by: Francesco Zanella <franzghosts@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hassay Ádám Tamás <hassayadam@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 <jakubfabijan@tuta.io>
Co-authored-by: Junan Chk <junanchakma2000@gmail.com>
Co-authored-by: K. Sz. Bence <tudi20@protonmail.com>
Co-authored-by: Kaleb <kalebcarvalho1@gmail.com>
Co-authored-by: Krishna Chand <krishna_chand67@naver.com>
Co-authored-by: LoneHash <sameepsk2@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Miguel Alexandro Manzano Guerra <kuro_eis@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nishant Bodkhe <nishantbodkhe44@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Pedro <pedro-mediavilla@hotmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Redy Apriyadi <redy.apriyadi@gmail.com>
Co-authored-by: Ric <rikku.debec@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Sieg Jaeger <zekerett@gmail.com>
Co-authored-by: Steven Pedroza <stevenpedroza56@gmail.com>
Co-authored-by: Temporary Person <TemporaryPerson@protonmail.com>
Co-authored-by: Zero O <godarms2010@live.com>
Co-authored-by: ZiomaleQ <r.partyka30@gmail.com>
Co-authored-by: crackheadakira <lasn.mine@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: julptk <julptk8@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: phannhanhn201 <phannhanhn201@gmail.com>
Co-authored-by: rytis sertvytis <knysliukas2002@gmail.com>
Co-authored-by: soplatnik <jestapom@protonmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: พรหมชัย ชูแสง <promchai2sin@gmail.com>
2021-10-04 15:41:54 -04:00
675c0cefc3 Fix crash in single-page chapters 2021-10-04 11:06:23 -04:00
1a52385b78 Formatting 2021-10-04 10:50:13 -04:00
372e500590 Remove extra padding when using list with Per Category setting (#5997)
* Remove padding when using list with Per Category setting (fixes #5636)

* Add view type to RecyclerViewPagerAdapter

Correctly this time (ノ◕ヮ◕)ノ*:・゚✧

* Minor tweaks
2021-10-04 10:41:20 -04:00
cc1a317439 enable "ALL" in Browse by default (#6023)
some extensions, including self-hosted ones, have the "ALL" label and
sometimes users get confused with not having enabled "ALL" after
installing new extensions
2021-10-03 15:40:51 -04:00
6d650518a1 App-wide typography adjustments (#5931)
* Manga detail

Also adjust chapter item layout to accommodate bigger
display/font size

* Library

* Updates

* History

* Browse

* Preferences

* Button

* Navigation view

* category-download

* Google Sans

* Reader

* Chips

* Revert "Google Sans"

This reverts commit 5dd4c41f

* Misc

* Cleanups

* Section header text appearance

* Increase library manga title size

* Revert "Increase library manga title size"

This reverts commit 474be913

* Increase section header letter spacing

* Derps
2021-10-03 12:32:04 -04:00
7940117577 Sort and remove duplicates in genres (#6021)
* Sort and remove duplicates in genres

Co-authored-by: ivaniskandar <12537387+ivaniskandar@users.noreply.github.com>

* Remove Sort and filter out blank genre

Co-authored-by: ivaniskandar <12537387+ivaniskandar@users.noreply.github.com>
2021-10-03 12:19:37 -04:00
b0f87fdd21 LicensesController: Move item init to IO thread (#6020) 2021-10-03 12:00:00 -04:00
dc92ffed87 Switch to Material Slider in color filter settings 2021-10-03 11:58:52 -04:00
4af578e310 Apply navigation bar insets to fast scroller and settings search list (#6015) 2021-10-03 11:28:20 -04:00
e22825d818 Check if wifi is connected rather than enabled while downloading. (#5967)
* Fixxy Wixxy

* Downgrade check from Android S to Android Q
2021-10-03 11:27:56 -04:00
e2da6259e7 Update AboutLib plugin 2021-10-03 11:14:56 -04:00
d149017c60 Switch to Material Slider for reader seekbar
Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
2021-10-03 11:14:49 -04:00
afc400121b Update dependencies 2021-10-01 18:28:02 -04:00
ef993515c6 Fix MangaController toolbar title showing when editing category (#6005) 2021-10-01 17:52:06 -04:00
edb1d21ddc Don't bury sort menu in overflow in Migrate screen 2021-10-01 17:41:14 -04:00
ba8abd94a8 Ability to order sources by library count when migrating (#6000)
* order sources by library count when migrating (closes #4703)

* Use plain menu instead of full-on sheet
2021-10-01 17:37:43 -04:00
c6d4e4c15f Move extensions enabled languages on top (closes #5694) (#5998) 2021-10-01 09:15:04 -04:00
09f0ac866f Fix incorrect appbar lift state when opening MangaController in hidden state (#5990) 2021-10-01 09:13:00 -04:00
7ed25704d6 Add chapter bookmarking feature to Updates screen (#5984) 2021-10-01 08:11:31 -04:00
2196dac63e Fix variable name in isOnline (#5991) 2021-10-01 08:09:46 -04:00
c8f70efded ReaderActivity: Block focus on viewer (#5996) 2021-10-01 08:09:36 -04:00
ea97488670 Revert parseAs inline function change
Some people sometimes get compile issues?
2021-09-30 17:52:07 -04:00
c2255b0a0f Mark installer names as non-translatable 2021-09-25 21:08:31 -04:00
f754b081ce Use data class to parse extensions list 2021-09-25 14:57:54 -04:00
07771cb5e4 Update kotlinx.serialization 2021-09-25 14:41:48 -04:00
690d8e43ae Show message in migrate screen if library is empty 2021-09-25 14:41:35 -04:00
82f14a7d59 Hide soft keyboard after submitting search query throughout app (#5837)
* Clear focus from SearchView when submitting a search query in BrowseSourceController

* Revert "Clear focus from SearchView when submitting a search query"

* Implement SearchView focus clearing in Tachiyomi's subclass to enable feature throughout app

* Add support for keyboard Enter key

Pressing enter on a keyboard (when using the emulator for example) now also submits the query
2021-09-25 14:32:19 -04:00
b284384f0a Implement new extension install methods (#5904)
* Implement new extension install methods

* Fixes

* Resolve feedback

* Keep pending status when waiting to install

* Cancellable installation

* Remove auto error now that we have cancellable job
2021-09-25 14:31:52 -04:00
1ae0d1b5d0 Reattach after slight delay instead on every db update (#5956) 2021-09-23 18:45:55 -04:00
9de08c8166 Update dependencies 2021-09-20 14:33:35 -04:00
a2d007f2a9 Toolbar and bottom nav scroll snap (#5915) 2021-09-18 16:41:23 -04:00
774f818bbb Fix setting search re-animating on activity recreation (fixes #5882) 2021-09-18 16:28:58 -04:00
0ec7121b8f Adjust snackbar durations (closes #5932) 2021-09-18 16:17:07 -04:00
d7d46f4447 Minor cleanup 2021-09-18 16:13:14 -04:00
45fad147bf Remove spaces at end of line before removing multiple new lines (#5928) 2021-09-18 15:16:03 -04:00
3664195c71 rewrite getFormat the kotlin way (#5930) 2021-09-18 15:15:38 -04:00
fce3cd00a1 Remove setting to disable update error notifications and split out notification channel
Users can exclude things from updating if needed, or disable the notification channel from system settings.
2021-09-17 19:14:30 -04:00
33b3be0d0e Move extension app info button
Aligns with TachiyomiJ2K.
2021-09-16 17:57:41 -04:00
cfd1b4a6c6 Fix toolbar title alpha (#5910) 2021-09-16 17:39:13 -04:00
d45fefd6f0 handle maxNumberSort from API (#5917) 2021-09-16 17:37:42 -04:00
f125ab01ee Change how the bottom navigation is hidden (#5823)
* Change how the bottom navigation is hidden

Modifies the translationY instead of the height.

* Cleanups
2021-09-16 17:37:17 -04:00
be001d090c [skip ci] Update issue closer to ignore myanimelist (#5911)
Not sure if there's any limitation for the regex but this will ignore myanimelist strings, in practice.
2021-09-14 11:50:21 -04:00
971d8a7e40 Allow preferences to multi-line (#5905) 2021-09-13 18:39:14 -04:00
a2cf210a52 Unify NSFW flagging for sources/extensions
Since multisource extensions are no longer a thing, we now simply rely on the flag at the extension level, i.e. the per-Source/SourceFactory `@Nsfw` annotation is no longer checked.
We'll have to remove all of the annotation usages from the existing sources, which will also effectively break the setting for older versions of the app.
2021-09-13 17:49:58 -04:00
175 changed files with 3498 additions and 1761 deletions

View File

@ -3,7 +3,7 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.12.2) - To the latest version of the app (stable is v0.12.3)
- All extensions - All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/ - I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Tachiyomi version in **More → About**.
placeholder: | placeholder: |
Example: "0.12.2" Example: "0.12.3"
validations: validations:
required: true required: true
@ -98,7 +98,7 @@ body:
required: true required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.12.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -33,7 +33,7 @@ body:
required: true required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose). - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
required: true required: true
- label: I have updated the app to version **[0.12.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@ -25,7 +25,7 @@ jobs:
}, },
{ {
"type": "both", "type": "both",
"regex": ".*(aniyomi|anime).*", "regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
"ignoreCase": true, "ignoreCase": true,
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi" "message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
} }

View File

@ -29,8 +29,8 @@ android {
minSdk = AndroidConfig.minSdk minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk targetSdk = AndroidConfig.targetSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 68 versionCode = 69
versionName = "0.12.2" versionName = "0.12.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -138,19 +138,20 @@ dependencies {
implementation("org.tachiyomi:source-api:1.1") implementation("org.tachiyomi:source-api:1.1")
// AndroidX libraries // AndroidX libraries
implementation("androidx.annotation:annotation:1.3.0-alpha01") implementation("androidx.annotation:annotation:1.3.0-beta01")
implementation("androidx.appcompat:appcompat:1.4.0-alpha03") implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03") implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation("androidx.browser:browser:1.3.0") implementation("androidx.browser:browser:1.4.0-beta01")
implementation("androidx.constraintlayout:constraintlayout:2.1.0") implementation("androidx.constraintlayout:constraintlayout:2.1.1")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0") implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.7.0-alpha02") implementation("androidx.core:core-ktx:1.7.0-beta02")
implementation("androidx.core:core-splashscreen:1.0.0-alpha01") implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
val lifecycleVersion = "2.4.0-alpha01" val lifecycleVersion = "2.4.0-beta01"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
@ -174,7 +175,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.2") implementation("org.conscrypt:conscrypt-android:2.5.2")
// Data serialization (JSON, protobuf) // Data serialization (JSON, protobuf)
val kotlinSerializationVersion = "1.3.0-RC" val kotlinSerializationVersion = "1.3.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
@ -225,13 +226,15 @@ dependencies {
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1") implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// UI libraries // UI libraries
implementation("com.google.android.material:material:1.5.0-alpha03") implementation("com.google.android.material:material:1.5.0-alpha04")
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4") implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
implementation("eu.davidea:flexible-adapter:5.1.0") implementation("eu.davidea:flexible-adapter:5.1.0")
implementation("eu.davidea:flexible-adapter-ui:1.0.0") implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation("dev.chrisbanes.insetter:insetter:0.6.0") implementation("dev.chrisbanes.insetter:insetter:0.6.0")
// Conductor // Conductor
@ -258,6 +261,11 @@ dependencies {
// Licenses // Licenses
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}") implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Shizuku
val shizukuVersion = "12.0.0"
implementation("dev.rikka.shizuku:api:$shizukuVersion")
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
// Tests // Tests
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1") testImplementation("org.assertj:assertj-core:3.16.1")

View File

@ -18,6 +18,7 @@
<!-- For managing extensions --> <!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<!-- To view extension packages in API 30+ --> <!-- To view extension packages in API 30+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
@ -188,6 +189,9 @@
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false" /> android:exported="false" />
<service android:name=".extension.util.ExtensionInstallService"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@ -198,6 +202,14 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" /> android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut" <meta-data android:name="android.webkit.WebView.MetricsOptOut"

View File

@ -0,0 +1,100 @@
package com.google.android.material.appbar
import android.animation.ValueAnimator
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.ViewCompat
import androidx.core.view.marginTop
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.findChild
import eu.kanade.tachiyomi.widget.ElevationAppBarLayout
import kotlin.math.roundToLong
/**
* Hide toolbar on scroll behavior for [AppBarLayout].
*
* Inside this package to access some package-private methods.
*/
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
@ViewCompat.NestedScrollType
private var lastStartedType: Int = 0
private var offsetAnimator: ValueAnimator? = null
private var toolbarHeight: Int = 0
override fun onStartNestedScroll(
parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int
): Boolean {
lastStartedType = type
offsetAnimator?.cancel()
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
}
override fun onStopNestedScroll(
parent: CoordinatorLayout,
layout: AppBarLayout,
target: View,
type: Int
) {
super.onStopNestedScroll(parent, layout, target, type)
if (toolbarHeight == 0) {
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
}
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
animateToolbarVisibility(
parent,
layout,
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
)
}
}
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
super.onFlingFinished(parent, layout)
animateToolbarVisibility(
parent,
layout,
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
)
}
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
return topBottomOffsetForScrollingSibling - abl.marginTop
}
private fun animateToolbarVisibility(
coordinatorLayout: CoordinatorLayout,
child: AppBarLayout,
isVisible: Boolean
) {
val current = getTopBottomOffsetForScrollingSibling(child)
val target = if (isVisible) 0 else -toolbarHeight
if (current == target) return
offsetAnimator?.cancel()
offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator()
duration = (150 * child.context.animatorDurationScale).roundToLong()
addUpdateListener {
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
}
doOnEnd {
if ((child as? ElevationAppBarLayout)?.isTransparentWhenNotLifted == true) {
child.isLifted = !isVisible
}
}
setIntValues(current, target)
start()
}
}
}

View File

@ -12,9 +12,8 @@ import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.Lifecycle import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import coil.ImageLoader import coil.ImageLoader
@ -45,14 +44,14 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.security.Security import java.security.Security
open class App : Application(), LifecycleObserver, ImageLoaderFactory { open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver() private val disableIncognitoReceiver = DisableIncognitoReceiver()
override fun onCreate() { override fun onCreate() {
super.onCreate() super<Application>.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
// TLS 1.3 support for Android < 10 // TLS 1.3 support for Android < 10
@ -131,9 +130,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
}.build() }.build()
} }
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) override fun onStop(owner: LifecycleOwner) {
@Suppress("unused")
fun onAppBackgrounded() {
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) { if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
SecureActivityDelegate.locked = true SecureActivityDelegate.locked = true
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.annotations package eu.kanade.tachiyomi.annotations
// TODO: remove this when no longer used in extensions
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class Nsfw annotation class Nsfw

View File

@ -36,7 +36,8 @@ interface Manga : SManga {
} }
fun getGenres(): List<String>? { fun getGenres(): List<String>? {
return genre?.split(", ")?.map { it.trim() } if (genre.isNullOrBlank()) return null
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
} }
private fun setChapterFlags(flag: Int, mask: Int) { private fun setChapterFlags(flag: Int, mask: Int) {

View File

@ -15,11 +15,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.wifiManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import ru.beryukhov.reactivenetwork.ReactiveNetwork import ru.beryukhov.reactivenetwork.ReactiveNetwork
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@ -140,8 +141,9 @@ class DownloadService : Service() {
onNetworkStateChanged() onNetworkStateChanged()
} }
} }
.catch { .catch { error ->
withUIContext { withUIContext {
Timber.e(error)
toast(R.string.download_queue_error) toast(R.string.download_queue_error)
stopSelf() stopSelf()
} }
@ -154,7 +156,7 @@ class DownloadService : Service() {
*/ */
private fun onNetworkStateChanged() { private fun onNetworkStateChanged() {
if (isOnline()) { if (isOnline()) {
if (preferences.downloadOnlyOverWifi() && !wifiManager.isWifiEnabled) { if (preferences.downloadOnlyOverWifi() && !isConnectedToWifi()) {
stopDownloads(R.string.download_notifier_text_only_wifi) stopDownloads(R.string.download_notifier_text_only_wifi)
} else { } else {
val started = downloadManager.startDownloads() val started = downloadManager.startDownloads()

View File

@ -51,7 +51,7 @@ class LibraryUpdateNotifier(private val context: Context) {
* Cached progress notification to avoid creating a lot. * Cached progress notification to avoid creating a lot.
*/ */
val progressNotificationBuilder by lazy { val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) { context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
setContentTitle(context.getString(R.string.app_name)) setContentTitle(context.getString(R.string.app_name))
setSmallIcon(R.drawable.ic_refresh_24dp) setSmallIcon(R.drawable.ic_refresh_24dp)
setLargeIcon(notificationBitmap) setLargeIcon(notificationBitmap)
@ -101,7 +101,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.notificationManager.notify( context.notificationManager.notify(
Notifications.ID_LIBRARY_ERROR, Notifications.ID_LIBRARY_ERROR,
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) { context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size)) setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
setStyle( setStyle(
NotificationCompat.BigTextStyle().bigText( NotificationCompat.BigTextStyle().bigText(

View File

@ -350,7 +350,7 @@ class LibraryUpdateService(
} }
} }
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) { if (failedUpdates.isNotEmpty()) {
val errorFile = writeErrorFile(failedUpdates) val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification( notifier.showUpdateErrorNotification(
failedUpdates.map { it.first.title }, failedUpdates.map { it.first.title },

View File

@ -24,8 +24,10 @@ object Notifications {
/** /**
* Notification channel and ids used by the library updater. * Notification channel and ids used by the library updater.
*/ */
const val CHANNEL_LIBRARY = "library_channel" private const val GROUP_LIBRARY = "group_library"
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
const val ID_LIBRARY_PROGRESS = -101 const val ID_LIBRARY_PROGRESS = -101
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
const val ID_LIBRARY_ERROR = -102 const val ID_LIBRARY_ERROR = -102
/** /**
@ -51,6 +53,7 @@ object Notifications {
*/ */
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel" const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
const val ID_UPDATES_TO_EXTS = -401 const val ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402
/** /**
* Notification channel and ids used by the backup/restore system. * Notification channel and ids used by the backup/restore system.
@ -77,7 +80,8 @@ object Notifications {
private val deprecatedChannels = listOf( private val deprecatedChannels = listOf(
"downloader_channel", "downloader_channel",
"backup_restore_complete_channel" "backup_restore_complete_channel",
"library_channel",
) )
/** /**
@ -89,64 +93,75 @@ object Notifications {
fun createChannels(context: Context) { fun createChannels(context: Context) {
val notificationService = NotificationManagerCompat.from(context) val notificationService = NotificationManagerCompat.from(context)
val channelGroupList = listOf( notificationService.createNotificationChannelGroupsCompat(
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) { listOf(
setName(context.getString(R.string.group_backup_restore)) buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
}, setName(context.getString(R.string.label_backup))
buildNotificationChannelGroup(GROUP_DOWNLOADER) { },
setName(context.getString(R.string.group_downloader)) buildNotificationChannelGroup(GROUP_DOWNLOADER) {
} setName(context.getString(R.string.download_notifier_downloader_title))
},
buildNotificationChannelGroup(GROUP_LIBRARY) {
setName(context.getString(R.string.label_library))
},
)
) )
notificationService.createNotificationChannelGroupsCompat(channelGroupList)
val channelList = listOf( notificationService.createNotificationChannelsCompat(
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) { listOf(
setName(context.getString(R.string.channel_common)) buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
}, setName(context.getString(R.string.channel_common))
buildNotificationChannel(CHANNEL_LIBRARY, IMPORTANCE_LOW) { },
setName(context.getString(R.string.channel_library)) buildNotificationChannel(CHANNEL_LIBRARY_PROGRESS, IMPORTANCE_LOW) {
setShowBadge(false) setName(context.getString(R.string.channel_progress))
}, setGroup(GROUP_LIBRARY)
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) { setShowBadge(false)
setName(context.getString(R.string.channel_progress)) },
setGroup(GROUP_DOWNLOADER) buildNotificationChannel(CHANNEL_LIBRARY_ERROR, IMPORTANCE_LOW) {
setShowBadge(false) setName(context.getString(R.string.channel_errors))
}, setGroup(GROUP_LIBRARY)
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) { setShowBadge(false)
setName(context.getString(R.string.channel_complete)) },
setGroup(GROUP_DOWNLOADER) buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
setShowBadge(false) setName(context.getString(R.string.channel_new_chapters))
}, },
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) { buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
setName(context.getString(R.string.channel_errors)) setName(context.getString(R.string.channel_progress))
setGroup(GROUP_DOWNLOADER) setGroup(GROUP_DOWNLOADER)
setShowBadge(false) setShowBadge(false)
}, },
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) { buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
setName(context.getString(R.string.channel_new_chapters)) setName(context.getString(R.string.channel_complete))
}, setGroup(GROUP_DOWNLOADER)
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) { setShowBadge(false)
setName(context.getString(R.string.channel_ext_updates)) },
}, buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) { setName(context.getString(R.string.channel_errors))
setName(context.getString(R.string.channel_progress)) setGroup(GROUP_DOWNLOADER)
setGroup(GROUP_BACKUP_RESTORE) setShowBadge(false)
setShowBadge(false) },
}, buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) { setName(context.getString(R.string.channel_progress))
setName(context.getString(R.string.channel_complete)) setGroup(GROUP_BACKUP_RESTORE)
setGroup(GROUP_BACKUP_RESTORE) setShowBadge(false)
setShowBadge(false) },
setSound(null, null) buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
}, setName(context.getString(R.string.channel_complete))
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) { setGroup(GROUP_BACKUP_RESTORE)
setName(context.getString(R.string.channel_crash_logs)) setShowBadge(false)
}, setSound(null, null)
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) { },
setName(context.getString(R.string.pref_incognito_mode)) buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
}, setName(context.getString(R.string.channel_crash_logs))
},
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
setName(context.getString(R.string.pref_incognito_mode))
},
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
setName(context.getString(R.string.channel_ext_updates))
},
)
) )
notificationService.createNotificationChannelsCompat(channelList)
// Delete old notification channels // Delete old notification channels
deprecatedChannels.forEach(notificationService::deleteNotificationChannel) deprecatedChannels.forEach(notificationService::deleteNotificationChannel)

View File

@ -151,11 +151,12 @@ object PreferenceKeys {
const val librarySortingMode = "library_sorting_mode" const val librarySortingMode = "library_sorting_mode"
const val librarySortingDirection = "library_sorting_ascending" const val librarySortingDirection = "library_sorting_ascending"
const val migrationSortingMode = "pref_migration_sorting"
const val migrationSortingDirection = "pref_migration_direction"
const val automaticExtUpdates = "automatic_ext_updates" const val automaticExtUpdates = "automatic_ext_updates"
const val showNsfwSource = "show_nsfw_source" const val showNsfwSource = "show_nsfw_source"
const val showNsfwExtension = "show_nsfw_extension"
const val labelNsfwExtension = "label_nsfw_extension"
const val startScreen = "start_screen" const val startScreen = "start_screen"
@ -173,8 +174,6 @@ object PreferenceKeys {
const val autoUpdateTrackers = "auto_update_trackers" const val autoUpdateTrackers = "auto_update_trackers"
const val showLibraryUpdateErrors = "show_library_update_errors"
const val downloadNew = "download_new" const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories" const val downloadNewCategories = "download_new_categories"
@ -226,6 +225,8 @@ object PreferenceKeys {
const val tabletUiMode = "tablet_ui_mode" const val tabletUiMode = "tablet_ui_mode"
const val extensionInstaller = "extension_installer"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"

View File

@ -57,4 +57,10 @@ object PreferenceValues {
LANDSCAPE, LANDSCAPE,
NEVER, NEVER,
} }
enum class ExtensionInstaller {
LEGACY,
PACKAGEINSTALLER,
SHIZUKU
}
} }

View File

@ -12,11 +12,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.system.MiuiUtil
import eu.kanade.tachiyomi.util.system.isTablet import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -86,8 +88,6 @@ class PreferencesHelper(val context: Context) {
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false) fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, true)
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system) fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT) fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
@ -190,7 +190,7 @@ class PreferencesHelper(val context: Context) {
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID) fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language)) fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "") fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
@ -268,11 +268,12 @@ class PreferencesHelper(val context: Context) {
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL) fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING) fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true) fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true) fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0) fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
@ -325,6 +326,11 @@ class PreferencesHelper(val context: Context) {
if (context.applicationContext.isTablet()) Values.TabletUiMode.ALWAYS else Values.TabletUiMode.NEVER if (context.applicationContext.isTablet()) Values.TabletUiMode.ALWAYS else Values.TabletUiMode.NEVER
) )
fun extensionInstaller() = flowPrefs.getEnum(
Keys.extensionInstaller,
if (MiuiUtil.isMiui()) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
)
fun setChapterSettingsDefault(manga: Manga) { fun setChapterSettingsDefault(manga: Manga) {
prefs.edit { prefs.edit {
putInt(Keys.defaultChapterFilterByRead, manga.readFilter) putInt(Keys.defaultChapterFilterByRead, manga.readFilter)

View File

@ -47,7 +47,7 @@ class KomgaApi(private val client: OkHttpClient) {
track.apply { track.apply {
cover_url = "$url/thumbnail" cover_url = "$url/thumbnail"
tracking_url = url tracking_url = url
total_chapters = progress.booksCount total_chapters = progress.maxNumberSort.toInt()
status = when (progress.booksCount) { status = when (progress.booksCount) {
progress.booksUnreadCount -> Komga.UNREAD progress.booksUnreadCount -> Komga.UNREAD
progress.booksReadCount -> Komga.COMPLETED progress.booksReadCount -> Komga.COMPLETED

View File

@ -91,7 +91,8 @@ data class ReadProgressDto(
booksReadCount, booksReadCount,
booksUnreadCount, booksUnreadCount,
booksInProgressCount, booksInProgressCount,
lastReadContinuousIndex.toFloat() lastReadContinuousIndex.toFloat(),
booksCount.toFloat(),
) )
} }
@ -102,4 +103,5 @@ data class ReadProgressV2Dto(
val booksUnreadCount: Int, val booksUnreadCount: Int,
val booksInProgressCount: Int, val booksInProgressCount: Int,
val lastReadContinuousNumberSort: Float, val lastReadContinuousNumberSort: Float,
val maxNumberSort: Float,
) )

View File

@ -227,14 +227,26 @@ class ExtensionManager(
return installExtension(availableExt) return installExtension(availableExt)
} }
fun cancelInstallUpdateExtension(extension: Extension) {
installer.cancelInstall(extension.pkgName)
}
/** /**
* Sets the result of the installation of an extension. * Sets to "installing" status of an extension installation.
* *
* @param downloadId The id of the download. * @param downloadId The id of the download.
* @param result Whether the extension was installed or not.
*/ */
fun setInstalling(downloadId: Long) {
installer.updateInstallStep(downloadId, InstallStep.Installing)
}
fun setInstallationResult(downloadId: Long, result: Boolean) { fun setInstallationResult(downloadId: Long, result: Boolean) {
installer.setInstallationResult(downloadId, result) val step = if (result) InstallStep.Installed else InstallStep.Error
installer.updateInstallStep(downloadId, step)
}
fun updateInstallStep(downloadId: Long, step: InstallStep) {
installer.updateInstallStep(downloadId, step)
} }
/** /**

View File

@ -10,11 +10,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.Serializable
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date import java.util.Date
@ -28,8 +24,8 @@ internal class ExtensionGithubApi {
networkService.client networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json")) .newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await() .await()
.parseAs<JsonArray>() .parseAs<List<ExtensionJsonObject>>()
.let { parseResponse(it) } .toExtensions()
} }
} }
@ -56,24 +52,23 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate return extensionsWithUpdate
} }
private fun parseResponse(json: JsonArray): List<Extension.Available> { private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
return json return this
.filter { element -> .filter {
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content val libVersion = it.version.substringBeforeLast('.').toDouble()
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
} }
.map { element -> .map {
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ") Extension.Available(
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content name = it.name.substringAfter("Tachiyomi: "),
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content pkgName = it.pkg,
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content versionName = it.version,
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.long versionCode = it.code,
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content lang = it.lang,
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1 isNsfw = it.nsfw == 1,
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}" apkName = it.apk,
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) )
} }
} }
@ -83,3 +78,14 @@ internal class ExtensionGithubApi {
} }
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/" private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
@Serializable
private data class ExtensionJsonObject(
val name: String,
val pkg: String,
val apk: String,
val version: String,
val code: Long,
val lang: String,
val nsfw: Int,
)

View File

@ -0,0 +1,170 @@
package eu.kanade.tachiyomi.extension.installer
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep
import uy.kohesive.injekt.injectLazy
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
/**
* Base implementation class for extension installer. To be used inside a foreground [Service].
*/
abstract class Installer(private val service: Service) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
/**
* Installer readiness. If false, queue check will not run.
*
* @see checkQueue
*/
abstract var ready: Boolean
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, uri: Uri) {
queue.add(Entry(downloadId, uri))
checkQueue()
}
/**
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
* when the install process for this entry is finished to continue the queue.
*
* @param entry The [Entry] of item to process
* @see continueQueue
*/
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId)
}
/**
* Called before queue continues. Override this to handle when the removed entry is
* currently being processed.
*
* @return true if this entry can be removed from queue.
*/
open fun cancelEntry(entry: Entry): Boolean {
return true
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
checkQueue()
}
}
/**
* Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
service.stopSelf()
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeFirst()
processEntry(nextEntry)
}
}
/**
* Call this method when the provided service is destroyed.
*/
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
queue.clear()
waitingInstall.set(null)
}
protected fun getActiveEntry(): Entry? = waitingInstall.get()
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
}
}
/**
* Install item to queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
data class Entry(val downloadId: Long, val uri: Uri)
init {
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
}
companion object {
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
/**
* Attempts to cancel the installation entry for the provided download ID.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
fun cancelInstallQueue(context: Context, downloadId: Long) {
val intent = Intent(ACTION_CANCEL_QUEUE)
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
}
}

View File

@ -0,0 +1,105 @@
package eu.kanade.tachiyomi.extension.installer
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getUriSize
import timber.log.Timber
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
private val packageInstaller = service.packageManager.packageInstaller
private val packageActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (userAction == null) {
Timber.e("Fatal error for $intent")
continueQueue(InstallStep.Error)
return
}
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(userAction)
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
continueQueue(InstallStep.Idle)
}
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
else -> continueQueue(InstallStep.Error)
}
}
}
private var activeSession: Pair<Entry, Int>? = null
// Always ready
override var ready = true
override fun processEntry(entry: Entry) {
super.processEntry(entry)
activeSession = null
try {
val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
activeSession = entry to packageInstaller.createSession(installParams)
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
installParams.setSize(fileSize)
val inputStream = service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
val session = packageInstaller.openSession(activeSession!!.second)
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
session.use {
arrayOf(inputStream, outputStream).use {
inputStream.copyTo(outputStream)
session.fsync(outputStream)
}
val intentSender = PendingIntent.getBroadcast(
service,
activeSession!!.second,
Intent(INSTALL_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
).intentSender
session.commit(intentSender)
}
} catch (e: Exception) {
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
activeSession?.let { (_, sessionId) ->
packageInstaller.abandonSession(sessionId)
}
continueQueue(InstallStep.Error)
}
}
override fun cancelEntry(entry: Entry): Boolean {
activeSession?.let { (activeEntry, sessionId) ->
if (activeEntry == entry) {
packageInstaller.abandonSession(sessionId)
return false
}
}
return true
}
override fun onDestroy() {
service.unregisterReceiver(packageActionReceiver)
super.onDestroy()
}
init {
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
}
}
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"

View File

@ -0,0 +1,127 @@
package eu.kanade.tachiyomi.extension.installer
import android.app.Service
import android.content.pm.PackageManager
import android.os.Build
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.getUriSize
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku
import timber.log.Timber
import java.io.BufferedReader
import java.io.InputStream
class ShizukuInstaller(private val service: Service) : Installer(service) {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
Timber.e("Shizuku was killed prematurely")
service.stopSelf()
}
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
if (grantResult == PackageManager.PERMISSION_GRANTED) {
ready = true
checkQueue()
} else {
service.stopSelf()
}
Shizuku.removeRequestPermissionResultListener(this)
}
}
}
override var ready = false
@Suppress("BlockingMethodInNonBlockingContext")
override fun processEntry(entry: Entry) {
super.processEntry(entry)
ioScope.launch {
var sessionId: String? = null
try {
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
service.contentResolver.openInputStream(entry.uri)!!.use {
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
"pm install-create --user current -i ${service.packageName} -S $size"
} else {
"pm install-create -i ${service.packageName} -S $size"
}
val createResult = exec(createCommand)
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
?: throw RuntimeException("Failed to create install session")
val writeResult = exec("pm install-write -S $size $sessionId base -", it)
if (writeResult.resultCode != 0) {
throw RuntimeException("Failed to write APK to session $sessionId")
}
val commitResult = exec("pm install-commit $sessionId")
if (commitResult.resultCode != 0) {
throw RuntimeException("Failed to commit install session $sessionId")
}
continueQueue(InstallStep.Installed)
}
} catch (e: Exception) {
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
if (sessionId != null) {
exec("pm install-abandon $sessionId")
}
continueQueue(InstallStep.Error)
}
}
}
// Don't cancel if entry is already started installing
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
override fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
ioScope.cancel()
super.onDestroy()
}
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
@Suppress("DEPRECATION")
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
if (stdin != null) {
process.outputStream.use { stdin.copyTo(it) }
}
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
val resultCode = process.waitFor()
return ShellResult(resultCode, output)
}
private data class ShellResult(val resultCode: Int, val out: String)
init {
Shizuku.addBinderDeadListener(shizukuDeadListener)
ready = if (Shizuku.pingBinder()) {
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
true
} else {
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
false
}
} else {
Timber.e("Shizuku is not ready to use.")
service.toast(R.string.ext_installer_shizuku_stopped)
service.stopSelf()
false
}
}
}
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.extension.model package eu.kanade.tachiyomi.extension.model
enum class InstallStep { enum class InstallStep {
Pending, Downloading, Installing, Installed, Error; Idle, Pending, Downloading, Installing, Installed, Error;
fun isCompleted(): Boolean { fun isCompleted(): Boolean {
return this == Installed || this == Error return this == Installed || this == Error || this == Idle
} }
} }

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -40,10 +41,13 @@ class ExtensionInstallActivity : Activity() {
private fun checkInstallationResult(resultCode: Int) { private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID) val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
val success = resultCode == RESULT_OK
val extensionManager = Injekt.get<ExtensionManager>() val extensionManager = Injekt.get<ExtensionManager>()
extensionManager.setInstallationResult(downloadId, success) val newStep = when (resultCode) {
RESULT_OK -> InstallStep.Installed
RESULT_CANCELED -> InstallStep.Idle
else -> InstallStep.Error
}
extensionManager.updateInstallStep(downloadId, newStep)
} }
} }

View File

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.extension.util
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.notificationBuilder
import timber.log.Timber
class ExtensionInstallService : Service() {
private var installer: Installer? = null
override fun onCreate() {
super.onCreate()
val notification = notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false)
setOngoing(true)
setShowWhen(false)
setContentTitle(getString(R.string.ext_install_service_notif))
setProgress(100, 100, true)
}.build()
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.data
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
val installerUsed = intent?.getSerializableExtra(EXTRA_INSTALLER) as? PreferenceValues.ExtensionInstaller
if (uri == null || id == null || installerUsed == null) {
stopSelf()
return START_NOT_STICKY
}
if (installer == null) {
installer = when (installerUsed) {
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
PreferenceValues.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
else -> {
Timber.e("Not implemented for installer $installerUsed")
stopSelf()
return START_NOT_STICKY
}
}
}
installer!!.addToQueue(id, uri)
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
installer?.onDestroy()
installer = null
}
override fun onBind(i: Intent?): IBinder? = null
companion object {
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
fun getIntent(
context: Context,
downloadId: Long,
uri: Uri,
installer: PreferenceValues.ExtensionInstaller
): Intent {
return Intent(context, ExtensionInstallService::class.java)
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.putExtra(EXTRA_INSTALLER, installer)
}
}
}

View File

@ -7,15 +7,21 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -47,6 +53,8 @@ internal class ExtensionInstaller(private val context: Context) {
*/ */
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>() private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
private val installerPref = Injekt.get<PreferencesHelper>().extensionInstaller()
/** /**
* Adds the given extension to the downloads queue and returns an observable containing its * Adds the given extension to the downloads queue and returns an observable containing its
* step in the installation process. * step in the installation process.
@ -79,8 +87,6 @@ internal class ExtensionInstaller(private val context: Context) {
.map { it.second } .map { it.second }
// Poll download status // Poll download status
.mergeWith(pollStatus(id)) .mergeWith(pollStatus(id))
// Force an error if the download takes more than 3 minutes
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
// Stop when the application is installed or errors // Stop when the application is installed or errors
.takeUntil { it.isCompleted() } .takeUntil { it.isCompleted() }
// Always notify on main thread // Always notify on main thread
@ -126,12 +132,29 @@ internal class ExtensionInstaller(private val context: Context) {
* @param uri The uri of the extension to install. * @param uri The uri of the extension to install.
*/ */
fun installApk(downloadId: Long, uri: Uri) { fun installApk(downloadId: Long, uri: Uri) {
val intent = Intent(context, ExtensionInstallActivity::class.java) when (val installer = installerPref.get()) {
.setDataAndType(uri, APK_MIME) PreferenceValues.ExtensionInstaller.LEGACY -> {
.putExtra(EXTRA_DOWNLOAD_ID, downloadId) val intent = Intent(context, ExtensionInstallActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent) context.startActivity(intent)
}
else -> {
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
ContextCompat.startForegroundService(context, intent)
}
}
}
/**
* Cancels extension install and remove from download manager and installer.
*/
fun cancelInstall(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName) ?: return
downloadManager.remove(downloadId)
Installer.cancelInstallQueue(context, downloadId)
} }
/** /**
@ -147,13 +170,12 @@ internal class ExtensionInstaller(private val context: Context) {
} }
/** /**
* Sets the result of the installation of an extension. * Sets the step of the installation of an extension.
* *
* @param downloadId The id of the download. * @param downloadId The id of the download.
* @param result Whether the extension was installed or not. * @param step New install step.
*/ */
fun setInstallationResult(downloadId: Long, result: Boolean) { fun updateInstallStep(downloadId: Long, step: InstallStep) {
val step = if (result) InstallStep.Installed else InstallStep.Error
downloadsRelay.call(downloadId to step) downloadsRelay.call(downloadId to step)
} }
@ -216,9 +238,7 @@ internal class ExtensionInstaller(private val context: Context) {
val uri = downloadManager.getUriForDownloadedFile(id) val uri = downloadManager.getUriForDownloadedFile(id)
// Set next installation step // Set next installation step
if (uri != null) { if (uri == null) {
downloadsRelay.call(id to InstallStep.Installing)
} else {
Timber.e("Couldn't locate downloaded APK") Timber.e("Couldn't locate downloaded APK")
downloadsRelay.call(id to InstallStep.Error) downloadsRelay.call(id to InstallStep.Error)
return return

View File

@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@ -154,13 +153,7 @@ internal object ExtensionLoader {
try { try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) { when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is Source -> listOf(obj) is Source -> listOf(obj)
is SourceFactory -> { is SourceFactory -> obj.createSources()
if (isSourceNsfw(obj)) {
emptyList()
} else {
obj.createSources()
}
}
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -168,7 +161,6 @@ internal object ExtensionLoader {
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
.filter { !isSourceNsfw(it) }
val langs = sources.filterIsInstance<CatalogueSource>() val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang } .map { it.lang }
@ -215,22 +207,4 @@ internal object ExtensionLoader {
null null
} }
} }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
private fun isSourceNsfw(clazz: Any): Boolean {
if (loadNsfwSource) {
return false
}
if (clazz !is Source && clazz !is SourceFactory) {
return false
}
// Annotations are proxied, hence this janky way of checking for them
return clazz.javaClass.annotations
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
.firstOrNull { it == Nsfw::class.java.simpleName } != null
}
} }

View File

@ -271,18 +271,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
throw Exception(context.getString(R.string.chapter_not_found)) throw Exception(context.getString(R.string.chapter_not_found))
} }
private fun getFormat(file: File): Format { private fun getFormat(file: File) = with(file) {
val extension = file.extension when {
return if (file.isDirectory) { isDirectory -> Format.Directory(this)
Format.Directory(file) extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
} else if (extension.equals("zip", true) || extension.equals("cbz", true)) { extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
Format.Zip(file) extension.equals("epub", true) -> Format.Epub(this)
} else if (extension.equals("rar", true) || extension.equals("cbr", true)) { else -> throw Exception(context.getString(R.string.local_invalid_format))
Format.Rar(file)
} else if (extension.equals("epub", true)) {
Format.Epub(file)
} else {
throw Exception(context.getString(R.string.local_invalid_format))
} }
} }

View File

@ -11,7 +11,6 @@ import rx.Observable
open class SourceManager(private val context: Context) { open class SourceManager(private val context: Context) {
private val sourcesMap = mutableMapOf<Long, Source>() private val sourcesMap = mutableMapOf<Long, Source>()
private val stubSourcesMap = mutableMapOf<Long, StubSource>() private val stubSourcesMap = mutableMapOf<Long, StubSource>()
init { init {

View File

@ -22,5 +22,6 @@ class ExtensionAdapter(controller: ExtensionController) :
interface OnButtonClickListener { interface OnButtonClickListener {
fun onButtonClick(position: Int) fun onButtonClick(position: Int)
fun onCancelButtonClick(position: Int)
} }
} }

View File

@ -119,6 +119,11 @@ open class ExtensionController :
} }
} }
override fun onCancelButtonClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
presenter.cancelInstallUpdateExtension(extension)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.browse_extensions, menu) inflater.inflate(R.menu.browse_extensions, menu)

View File

@ -1,31 +1,28 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension
import android.view.View import android.view.View
import androidx.core.view.isVisible
import coil.clear import coil.clear
import coil.load import coil.load
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
FlexibleViewHolder(view, adapter) { FlexibleViewHolder(view, adapter) {
private val binding = ExtensionCardItemBinding.bind(view) private val binding = ExtensionCardItemBinding.bind(view)
private val shouldLabelNsfw by lazy {
Injekt.get<PreferencesHelper>().labelNsfwExtension()
}
init { init {
binding.extButton.setOnClickListener { binding.extButton.setOnClickListener {
adapter.buttonClickListener.onButtonClick(bindingAdapterPosition) adapter.buttonClickListener.onButtonClick(bindingAdapterPosition)
} }
binding.cancelButton.setOnClickListener {
adapter.buttonClickListener.onCancelButtonClick(bindingAdapterPosition)
}
} }
fun bind(item: ExtensionItem) { fun bind(item: ExtensionItem) {
@ -38,7 +35,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted) extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial) extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete) extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short) extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
else -> "" else -> ""
}.uppercase() }.uppercase()
@ -48,44 +45,40 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
} else { } else {
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) } extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
} }
bindButton(item) bindButtons(item)
} }
@Suppress("ResourceType") @Suppress("ResourceType")
fun bindButton(item: ExtensionItem) = with(binding.extButton) { fun bindButtons(item: ExtensionItem) = with(binding.extButton) {
isEnabled = true
isClickable = true
val extension = item.extension val extension = item.extension
val installStep = item.installStep val installStep = item.installStep
if (installStep != null) { setText(
setText( when (installStep) {
when (installStep) { InstallStep.Pending -> R.string.ext_pending
InstallStep.Pending -> R.string.ext_pending InstallStep.Downloading -> R.string.ext_downloading
InstallStep.Downloading -> R.string.ext_downloading InstallStep.Installing -> R.string.ext_installing
InstallStep.Installing -> R.string.ext_installing InstallStep.Installed -> R.string.ext_installed
InstallStep.Installed -> R.string.ext_installed InstallStep.Error -> R.string.action_retry
InstallStep.Error -> R.string.action_retry InstallStep.Idle -> {
} when (extension) {
) is Extension.Installed -> {
if (installStep != InstallStep.Error) { if (extension.hasUpdate) {
isEnabled = false R.string.ext_update
isClickable = false } else {
} R.string.action_settings
} else if (extension is Extension.Installed) { }
when { }
extension.hasUpdate -> { is Extension.Untrusted -> R.string.ext_trust
setText(R.string.ext_update) is Extension.Available -> R.string.ext_install
} }
else -> {
setText(R.string.action_settings)
} }
} }
} else if (extension is Extension.Untrusted) { )
setText(R.string.ext_trust)
} else { val isIdle = installStep == InstallStep.Idle || installStep == InstallStep.Error
setText(R.string.ext_install) binding.cancelButton.isVisible = !isIdle
} isEnabled = isIdle
isClickable = isIdle
} }
} }

View File

@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
data class ExtensionItem( data class ExtensionItem(
val extension: Extension, val extension: Extension,
val header: ExtensionGroupItem? = null, val header: ExtensionGroupItem? = null,
val installStep: InstallStep? = null val installStep: InstallStep = InstallStep.Idle
) : ) :
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) { AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
@ -49,7 +49,7 @@ data class ExtensionItem(
if (payloads == null || payloads.isEmpty()) { if (payloads == null || payloads.isEmpty()) {
holder.bind(this) holder.bind(this)
} else { } else {
holder.bindButton(this) holder.bindButtons(this)
} }
} }

View File

@ -55,14 +55,14 @@ open class ExtensionPresenter(
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> { private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().get() val activeLangs = preferences.enabledLanguages().get()
val showNsfwExtensions = preferences.showNsfwExtension().get() val showNsfwSources = preferences.showNsfwSource().get()
val (installed, untrusted, available) = tuple val (installed, untrusted, available) = tuple
val items = mutableListOf<ExtensionItem>() val items = mutableListOf<ExtensionItem>()
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name } val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedBy { it.name }
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name })) val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
val untrustedSorted = untrusted.sortedBy { it.name } val untrustedSorted = untrusted.sortedBy { it.name }
val availableSorted = available val availableSorted = available
// Filter out already installed extensions and disabled languages // Filter out already installed extensions and disabled languages
@ -70,21 +70,21 @@ open class ExtensionPresenter(
installed.none { it.pkgName == avail.pkgName } && installed.none { it.pkgName == avail.pkgName } &&
untrusted.none { it.pkgName == avail.pkgName } && untrusted.none { it.pkgName == avail.pkgName } &&
(avail.lang in activeLangs || avail.lang == "all") && (avail.lang in activeLangs || avail.lang == "all") &&
(showNsfwExtensions || !avail.isNsfw) (showNsfwSources || !avail.isNsfw)
} }
.sortedBy { it.name } .sortedBy { it.name }
if (updatesSorted.isNotEmpty()) { if (updatesSorted.isNotEmpty()) {
val header = ExtensionGroupItem(context.getString(R.string.ext_updates_pending), updatesSorted.size, true) val header = ExtensionGroupItem(context.getString(R.string.ext_updates_pending), updatesSorted.size, true)
items += updatesSorted.map { extension -> items += updatesSorted.map { extension ->
ExtensionItem(extension, header, currentDownloads[extension.pkgName]) ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
} }
} }
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) { if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size) val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
items += installedSorted.map { extension -> items += installedSorted.map { extension ->
ExtensionItem(extension, header, currentDownloads[extension.pkgName]) ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
} }
items += untrustedSorted.map { extension -> items += untrustedSorted.map { extension ->
@ -100,7 +100,7 @@ open class ExtensionPresenter(
.forEach { .forEach {
val header = ExtensionGroupItem(it.key, it.value.size) val header = ExtensionGroupItem(it.key, it.value.size)
items += it.value.map { extension -> items += it.value.map { extension ->
ExtensionItem(extension, header, currentDownloads[extension.pkgName]) ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
} }
} }
} }
@ -133,6 +133,10 @@ open class ExtensionPresenter(
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension) extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
} }
fun cancelInstallUpdateExtension(extension: Extension) {
extensionManager.cancelInstallUpdateExtension(extension)
}
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) { private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
this.doOnNext { currentDownloads[extension.pkgName] = it } this.doOnNext { currentDownloads[extension.pkgName] = it }
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) } .doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }

View File

@ -2,10 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@ -14,7 +11,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.preference.Preference
import androidx.preference.PreferenceGroupAdapter import androidx.preference.PreferenceGroupAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@ -68,7 +64,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
} }
override fun createPresenter(): ExtensionDetailsPresenter { override fun createPresenter(): ExtensionDetailsPresenter {
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!) return ExtensionDetailsPresenter(this, args.getString(PKGNAME_KEY)!!)
} }
override fun getTitle(): String? { override fun getTitle(): String? {
@ -106,72 +102,87 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1 val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
with(screen) { with(screen) {
extension.sources if (isMultiSource && isMultiLangSingleSource.not()) {
.groupBy { (it as CatalogueSource).lang } multiLanguagePreference(context, extension.sources)
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) }) } else {
.forEach { singleLanguagePreference(context, extension.sources)
val preferenceBlock = { }
it.value
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.lowercase() }))
.forEach { source ->
val sourcePrefs = mutableListOf<Preference>()
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
key = source.getPreferenceKey()
title = when {
isMultiSource && !isMultiLangSingleSource -> source.toString()
else -> LocaleHelper.getSourceDisplayName(it.key, context)
}
isPersistent = false
isChecked = source.isEnabled()
onChange { newValue ->
val checked = newValue as Boolean
toggleSource(source, checked)
true
}
// React to enable/disable all changes
preferences.disabledSources().asFlow()
.onEach {
val enabled = source.isEnabled()
isChecked = enabled
sourcePrefs.forEach { pref -> pref.isVisible = enabled }
}
.launchIn(viewScope)
}
// Source enable/disable
if (source is ConfigurableSource) {
switchSettingsPreference {
block()
onSettingsClick = View.OnClickListener {
router.pushController(
SourcePreferencesController(source.id).withFadeTransaction()
)
}
}
} else {
switchPreference(block)
}
}
}
if (isMultiSource && !isMultiLangSingleSource) {
preferenceCategory {
title = LocaleHelper.getSourceDisplayName(it.key, context)
preferenceBlock()
}
} else {
preferenceBlock()
}
}
} }
return PreferenceGroupAdapter(screen) return PreferenceGroupAdapter(screen)
} }
private fun PreferenceScreen.singleLanguagePreference(context: Context, sources: List<Source>) {
sources
.map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
.sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
.forEach { (lang, source) ->
val preferenceBlock = {
sourceSwitchPreference(source, LocaleHelper.getSourceDisplayName(lang, context))
}
preferenceBlock()
}
}
private fun PreferenceScreen.multiLanguagePreference(context: Context, sources: List<Source>) {
sources
.groupBy { (it as CatalogueSource).lang }
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
.forEach { entry ->
val preferenceBlock = {
entry.value
.sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
.forEach { source ->
sourceSwitchPreference(source, source.toString())
}
}
preferenceCategory {
title = LocaleHelper.getSourceDisplayName(entry.key, context)
preferenceBlock()
}
}
}
private fun PreferenceScreen.sourceSwitchPreference(source: Source, name: String) {
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
key = source.getPreferenceKey()
title = name
isPersistent = false
isChecked = source.isEnabled()
onChange { newValue ->
val checked = newValue as Boolean
toggleSource(source, checked)
true
}
// React to enable/disable all changes
preferences.disabledSources().asFlow()
.onEach {
val enabled = source.isEnabled()
isChecked = enabled
}
.launchIn(viewScope)
}
// Source enable/disable
if (source is ConfigurableSource) {
switchSettingsPreference {
block()
onSettingsClick = View.OnClickListener {
router.pushController(
SourcePreferencesController(source.id).withFadeTransaction()
)
}
}
} else {
switchPreference(block)
}
}
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
preferenceScreen = null preferenceScreen = null
super.onDestroyView(view) super.onDestroyView(view)
@ -188,7 +199,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
R.id.action_history -> openCommitHistory() R.id.action_history -> openCommitHistory()
R.id.action_enable_all -> toggleAllSources(true) R.id.action_enable_all -> toggleAllSources(true)
R.id.action_disable_all -> toggleAllSources(false) R.id.action_disable_all -> toggleAllSources(false)
R.id.action_open_in_settings -> openInSettings()
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
@ -219,13 +229,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
openInBrowser(url) openInBrowser(url)
} }
private fun openInSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", presenter.pkgName, null)
}
startActivity(intent)
}
private fun Source.isEnabled(): Boolean { private fun Source.isEnabled(): Boolean {
return id.toString() !in preferences.disabledSources().get() return id.toString() !in preferences.disabledSources().get()
} }

View File

@ -44,6 +44,9 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
binding.btnUninstall.clicks() binding.btnUninstall.clicks()
.onEach { presenter.uninstallExtension() } .onEach { presenter.uninstallExtension() }
.launchIn(presenter.presenterScope) .launchIn(presenter.presenterScope)
binding.btnAppInfo.clicks()
.onEach { presenter.openInSettings() }
.launchIn(presenter.presenterScope)
if (extension.isObsolete) { if (extension.isObsolete) {
binding.warningBanner.isVisible = true binding.warningBanner.isVisible = true

View File

@ -1,17 +1,21 @@
package eu.kanade.tachiyomi.ui.browse.extension.details package eu.kanade.tachiyomi.ui.browse.extension.details
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
class ExtensionDetailsPresenter( class ExtensionDetailsPresenter(
val pkgName: String, private val controller: ExtensionDetailsController,
private val extensionManager: ExtensionManager = Injekt.get() private val pkgName: String,
) : BasePresenter<ExtensionDetailsController>() { ) : BasePresenter<ExtensionDetailsController>() {
private val extensionManager: ExtensionManager by injectLazy()
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
@ -36,4 +40,11 @@ class ExtensionDetailsPresenter(
val extension = extension ?: return val extension = extension ?: return
extensionManager.uninstallExtension(extension.pkgName) extensionManager.uninstallExtension(extension.pkgName)
} }
fun openInSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", pkgName, null)
}
controller.startActivity(intent)
}
} }

View File

@ -9,16 +9,20 @@ import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import uy.kohesive.injekt.injectLazy
class MigrationSourcesController : class MigrationSourcesController :
NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(), NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(),
FlexibleAdapter.OnItemClickListener { FlexibleAdapter.OnItemClickListener {
private val preferences: PreferencesHelper by injectLazy()
private var adapter: SourceAdapter? = null private var adapter: SourceAdapter? = null
init { init {
@ -56,13 +60,39 @@ class MigrationSourcesController :
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (val itemId = item.itemId) {
R.id.action_source_migration_help -> activity?.openInBrowser(HELP_URL) R.id.action_source_migration_help -> activity?.openInBrowser(HELP_URL)
R.id.asc_alphabetical, R.id.desc_alphabetical -> {
setSortingDirection(SortSetting.ALPHABETICAL, itemId == R.id.asc_alphabetical)
}
R.id.asc_count, R.id.desc_count -> {
setSortingDirection(SortSetting.TOTAL, itemId == R.id.asc_count)
}
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun setSortingDirection(sortSetting: SortSetting, isAscending: Boolean) {
val direction = if (isAscending) {
DirectionSetting.ASCENDING
} else {
DirectionSetting.DESCENDING
}
preferences.migrationSortingDirection().set(direction)
preferences.migrationSortingMode().set(sortSetting)
presenter.requestSortUpdate()
}
fun setSources(sourcesWithManga: List<SourceItem>) { fun setSources(sourcesWithManga: List<SourceItem>) {
// Show empty view if needed
if (sourcesWithManga.isNotEmpty()) {
binding.emptyView.hide()
} else {
binding.emptyView.show(R.string.information_empty_library)
}
adapter?.updateDataSet(sourcesWithManga) adapter?.updateDataSet(sourcesWithManga)
} }
@ -72,6 +102,16 @@ class MigrationSourcesController :
parentController!!.router.pushController(controller.withFadeTransaction()) parentController!!.router.pushController(controller.withFadeTransaction())
return false return false
} }
enum class DirectionSetting {
ASCENDING,
DESCENDING;
}
enum class SortSetting {
ALPHABETICAL,
TOTAL;
}
} }
private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/" private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"

View File

@ -1,25 +1,38 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.combineLatest
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.Collator
import java.util.Collections
import java.util.Locale
class MigrationSourcesPresenter( class MigrationSourcesPresenter(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<MigrationSourcesController>() { ) : BasePresenter<MigrationSourcesController>() {
private val preferences: PreferencesHelper by injectLazy()
private val sortRelay = BehaviorRelay.create(Unit)
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
db.getFavoriteMangas() db.getFavoriteMangas()
.asRxObservable() .asRxObservable()
.combineLatest(sortRelay.observeOn(Schedulers.io())) { sources, _ -> sources }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.map { findSourcesWithManga(it) } .map { findSourcesWithManga(it) }
.subscribeLatestCache(MigrationSourcesController::setSources) .subscribeLatestCache(MigrationSourcesController::setSources)
@ -34,7 +47,36 @@ class MigrationSourcesPresenter(
val source = sourceManager.getOrStub(it.key) val source = sourceManager.getOrStub(it.key)
SourceItem(source, it.value.size, header) SourceItem(source, it.value.size, header)
} }
.sortedBy { it.source.name.lowercase() } .sortedWith(sortFn())
.toList() .toList()
} }
fun sortFn(): java.util.Comparator<SourceItem> {
val sort by lazy {
preferences.migrationSortingMode().get()
}
val direction by lazy {
preferences.migrationSortingDirection().get()
}
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
}
val sortFn: (SourceItem, SourceItem) -> Int = { a, b ->
when (sort) {
MigrationSourcesController.SortSetting.ALPHABETICAL -> collator.compare(a.source.name.lowercase(locale), b.source.name.lowercase(locale))
MigrationSourcesController.SortSetting.TOTAL -> a.mangaCount.compareTo(b.mangaCount)
}
}
return when (direction) {
MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn)
MigrationSourcesController.DirectionSetting.DESCENDING -> Collections.reverseOrder(sortFn)
}
}
fun requestSortUpdate() {
sortRelay.call(Unit)
}
} }

View File

@ -195,7 +195,7 @@ class CategoryController :
(activity as? MainActivity)?.binding?.rootCoordinator!!, (activity as? MainActivity)?.binding?.rootCoordinator!!,
R.string.snack_categories_deleted, R.string.snack_categories_deleted,
R.string.action_undo, R.string.action_undo,
3000 4000
) )
mode.finish() mode.finish()

View File

@ -6,6 +6,7 @@ import android.view.ViewGroup
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -45,15 +46,18 @@ class LibraryAdapter(
private var boundViews = arrayListOf<View>() private var boundViews = arrayListOf<View>()
private val isPerCategory by lazy { preferences.categorisedDisplaySettings().get() }
private val currentDisplayMode by lazy { preferences.libraryDisplayMode().get() }
/** /**
* Creates a new view for this adapter. * Creates a new view for this adapter.
* *
* @return a new view. * @return a new view.
*/ */
override fun createView(container: ViewGroup): View { override fun inflateView(container: ViewGroup, viewType: Int): View {
val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false) val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
val view: LibraryCategoryView = binding.root val view: LibraryCategoryView = binding.root
view.onCreate(controller, binding) view.onCreate(controller, binding, viewType)
return view return view
} }
@ -120,4 +124,26 @@ class LibraryAdapter(
} }
} }
} }
override fun getViewType(position: Int): Int {
val category = categories[position]
return if (isPerCategory && category.id != 0) {
if (DisplayModeSetting.fromFlag(category.displayMode) == DisplayModeSetting.LIST) {
LIST_DISPLAY_MODE
} else {
GRID_DISPLAY_MODE
}
} else {
if (currentDisplayMode == DisplayModeSetting.LIST) {
LIST_DISPLAY_MODE
} else {
GRID_DISPLAY_MODE
}
}
}
companion object {
const val LIST_DISPLAY_MODE = 1
const val GRID_DISPLAY_MODE = 2
}
} }

View File

@ -5,14 +5,14 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.Insetter
import dev.chrisbanes.insetter.windowInsetTypesOf
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.lang.plusAssign
@ -27,9 +27,7 @@ import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.recyclerview.scrollStateChanges import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
import java.util.ArrayDeque import java.util.ArrayDeque
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting as DisplayMode
/** /**
* Fragment containing the library manga for a certain category. * Fragment containing the library manga for a certain category.
@ -41,8 +39,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
private val scope = MainScope() private val scope = MainScope()
private val preferences: PreferencesHelper by injectLazy()
/** /**
* The fragment containing this view. * The fragment containing this view.
*/ */
@ -71,12 +67,10 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
private var lastClickPositionStack = ArrayDeque(listOf(-1)) private var lastClickPositionStack = ArrayDeque(listOf(-1))
fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding) { fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding, viewType: Int) {
this.controller = controller this.controller = controller
recycler = if (preferences.libraryDisplayMode().get() == DisplayMode.LIST && recycler = if (viewType == LibraryAdapter.LIST_DISPLAY_MODE) {
!preferences.categorisedDisplaySettings().get()
) {
(binding.swipeRefresh.inflate(R.layout.library_list_recycler) as AutofitRecyclerView).apply { (binding.swipeRefresh.inflate(R.layout.library_list_recycler) as AutofitRecyclerView).apply {
spanCount = 1 spanCount = 1
} }
@ -86,11 +80,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
} }
recycler.applyInsetter { Insetter.builder()
type(navigationBars = true) { .paddingBottom(windowInsetTypesOf(navigationBars = true))
padding() .applyToView(recycler)
}
}
adapter = LibraryCategoryAdapter(this) adapter = LibraryCategoryAdapter(this)
@ -129,15 +121,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun onBind(category: Category) { fun onBind(category: Category) {
this.category = category this.category = category
// If displayMode should be set from category adjust manga count per row
if (preferences.categorisedDisplaySettings().get()) {
recycler.spanCount = if (DisplayMode.fromFlag(category.displayMode) == DisplayMode.LIST || (preferences.libraryDisplayMode().get() == DisplayMode.LIST && category.id == 0)) {
1
} else {
controller.mangaPerRow
}
}
adapter.mode = if (controller.selectedMangas.isNotEmpty()) { adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
SelectableAdapter.Mode.MULTI SelectableAdapter.Mode.MULTI
} else { } else {

View File

@ -42,9 +42,12 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.viewpager.pageSelections import reactivecircus.flowbinding.viewpager.pageSelections
import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class LibraryController( class LibraryController(
bundle: Bundle? = null, bundle: Bundle? = null,
@ -199,10 +202,12 @@ class LibraryController(
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged() is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
is LibrarySettingsSheet.Display.DisplayGroup -> { is LibrarySettingsSheet.Display.DisplayGroup -> {
if (!preferences.categorisedDisplaySettings().get() || activeCategory == 0) { val delay = if (preferences.categorisedDisplaySettings().get()) 125L else 0L
// Reattach adapter when flow preference change
reattachAdapter() Observable.timer(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
} .subscribe {
reattachAdapter()
}
} }
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged() is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged() is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
@ -299,11 +304,6 @@ class LibraryController(
.map { (it.id ?: -1) to (mangaMap[it.id]?.size ?: 0) } .map { (it.id ?: -1) to (mangaMap[it.id]?.size ?: 0) }
.toMap() .toMap()
if (preferences.categorisedDisplaySettings().get()) {
// Reattach adapter so it doesn't get de-synced
reattachAdapter()
}
// Restore active category. // Restore active category.
binding.libraryPager.setCurrentItem(activeCat, false) binding.libraryPager.setCurrentItem(activeCat, false)
@ -383,7 +383,7 @@ class LibraryController(
actionMode!!, actionMode!!,
R.menu.library_selection R.menu.library_selection
) { onActionItemClicked(it!!) } ) { onActionItemClicked(it!!) }
(activity as? MainActivity)?.showBottomNav(visible = false, expand = true) (activity as? MainActivity)?.showBottomNav(false)
} }
} }
@ -492,7 +492,7 @@ class LibraryController(
selectionRelay.call(LibrarySelectionEvent.Cleared()) selectionRelay.call(LibrarySelectionEvent.Cleared())
binding.actionToolbar.hide() binding.actionToolbar.hide()
(activity as? MainActivity)?.showBottomNav(visible = true, expand = true) (activity as? MainActivity)?.showBottomNav(true)
actionMode = null actionMode = null
} }

View File

@ -10,7 +10,6 @@ import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@ -64,7 +63,6 @@ import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isTablet import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
import eu.kanade.tachiyomi.widget.HideBottomNavigationOnScrollBehavior
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -86,8 +84,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
} }
} }
private var bottomNavAnimator: ViewHeightAnimator? = null
private var isConfirmingExit: Boolean = false private var isConfirmingExit: Boolean = false
private var isHandlingShortcut: Boolean = false private var isHandlingShortcut: Boolean = false
@ -138,15 +134,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
} }
setSplashScreenExitAnimation(splashScreen) setSplashScreenExitAnimation(splashScreen)
if (binding.bottomNav != null) {
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav!!)
// Set behavior of bottom nav
preferences.hideBottomBarOnScroll()
.asImmediateFlow { setBottomNavBehaviorOnScroll() }
.launchIn(lifecycleScope)
}
if (binding.sideNav != null) { if (binding.sideNav != null) {
preferences.sideNavIconAlignment() preferences.sideNavIconAlignment()
.asImmediateFlow { .asImmediateFlow {
@ -532,11 +519,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
binding.appbar.setExpanded(true) binding.appbar.setExpanded(true)
if ((from == null || from is RootController) && to !is RootController) { if ((from == null || from is RootController) && to !is RootController) {
showNav(visible = false, expand = true) showNav(false)
} }
if (to is RootController) { if (to is RootController) {
// Always show bottom nav again when returning to a RootController // Always show bottom nav again when returning to a RootController
showNav(visible = true, expand = from !is RootController) showNav(true)
} }
if (from is TabbedController) { if (from is TabbedController) {
@ -587,27 +574,22 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
} }
} }
private fun showNav(visible: Boolean, expand: Boolean = false) { private fun showNav(visible: Boolean) {
showBottomNav(visible, expand) showBottomNav(visible)
showSideNav(visible) showSideNav(visible)
} }
// Also used from some controllers to swap bottom nav with action toolbar // Also used from some controllers to swap bottom nav with action toolbar
fun showBottomNav(visible: Boolean, expand: Boolean = false) { fun showBottomNav(visible: Boolean) {
if (visible) { if (visible) {
binding.bottomNav?.translationY = 0F binding.bottomNav?.slideUp()
if (expand) {
bottomNavAnimator?.expand()
}
} else { } else {
bottomNavAnimator?.collapse() binding.bottomNav?.slideDown()
} }
} }
private fun showSideNav(visible: Boolean) { private fun showSideNav(visible: Boolean) {
binding.sideNav?.let { binding.sideNav?.isVisible = visible
it.isVisible = visible
}
} }
/** /**
@ -622,18 +604,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
} }
} }
private fun setBottomNavBehaviorOnScroll() {
showNav(visible = true)
binding.bottomNav?.updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = when {
preferences.hideBottomBarOnScroll().get() -> HideBottomNavigationOnScrollBehavior()
else -> null
}
}
binding.bottomNav?.translationY = 0F
}
private val nav: NavigationBarView private val nav: NavigationBarView
get() = binding.bottomNav ?: binding.sideNav!! get() = binding.bottomNav ?: binding.sideNav!!

View File

@ -1,107 +0,0 @@
package eu.kanade.tachiyomi.ui.main
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator
import androidx.annotation.Keep
class ViewHeightAnimator(val view: View, val duration: Long = 250L) {
/**
* The default height of the view. It's unknown until the view is layout.
*/
private var height = 0
/**
* Whether the last state of the view is shown or hidden.
*/
private var isLastStateShown = true
/**
* Animation used to expand and collapse the view.
*/
private val animation by lazy {
ObjectAnimator.ofInt(this, "height", height).apply {
duration = this@ViewHeightAnimator.duration
interpolator = DecelerateInterpolator()
}
}
init {
view.viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (view.height > 0) {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
// Save the tabs default height.
height = view.height
// Now that we know the height, set the initial height.
if (isLastStateShown) {
setHeight(height)
} else {
setHeight(0)
}
}
}
}
)
}
/**
* Sets the height of the tab layout.
*
* @param newHeight The new height of the tab layout.
*/
@Keep
fun setHeight(newHeight: Int) {
view.layoutParams.height = newHeight
view.requestLayout()
}
/**
* Returns the height of the tab layout. This method is also called from the animator through
* reflection.
*/
fun getHeight(): Int {
return view.layoutParams.height
}
/**
* Expands the tab layout with an animation.
*/
fun expand() {
if (isMeasured) {
if (getHeight() != height) {
animation.setIntValues(height)
animation.start()
} else {
animation.cancel()
}
}
isLastStateShown = true
}
/**
* Collapse the tab layout with an animation.
*/
fun collapse() {
if (isMeasured) {
if (getHeight() != 0) {
animation.setIntValues(0)
animation.start()
} else {
animation.cancel()
}
}
isLastStateShown = false
}
/**
* Returns whether the tab layout has a known height.
*/
private val isMeasured: Boolean
get() = height > 0
}

View File

@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
@ -92,10 +93,8 @@ import eu.kanade.tachiyomi.util.view.getCoordinates
import eu.kanade.tachiyomi.util.view.shrinkOnScroll import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.recyclerview.scrollEvents
import reactivecircus.flowbinding.recyclerview.scrollStateChanges import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import timber.log.Timber import timber.log.Timber
@ -179,7 +178,17 @@ class MangaController :
private var trackSheet: TrackSheet? = null private var trackSheet: TrackSheet? = null
private var dialog: MangaFullCoverDialog? = null private var dialog: DialogController? = null
/**
* For [recyclerViewUpdatesToolbarTitleAlpha]
*/
private var recyclerViewToolbarTitleAlphaUpdaterAdded = false
private val recyclerViewToolbarTitleAlphaUpdater = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
updateToolbarTitleAlpha()
}
}
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -191,15 +200,12 @@ class MangaController :
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
// Hide toolbar title on enter // Hide toolbar title on enter
if (type.isEnter) { // No need to update alpha for cover dialog
updateToolbarTitleAlpha() if (dialog == null) {
} else if (!type.isPush) { updateToolbarTitleAlpha(if (type.isEnter) 0F else 1F)
// Cancel listeners early
viewScope.cancel()
updateToolbarTitleAlpha(1F)
} }
recyclerViewUpdatesToolbarTitleAlpha(type.isEnter)
} }
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
@ -250,19 +256,15 @@ class MangaController :
binding.fullRecycler?.let { binding.fullRecycler?.let {
it.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter) it.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter)
it.scrollEvents()
.onEach { updateToolbarTitleAlpha() }
.launchIn(viewScope)
// Skips directly to chapters list if navigated to from the library // Skips directly to chapters list if navigated to from the library
it.post { it.post {
if (!fromSource && preferences.jumpToChapters()) { if (!fromSource && preferences.jumpToChapters()) {
(it.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0) val mainActivityAppBar = (activity as? MainActivity)?.binding?.appbar
} (it.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
1,
// Delayed in case we need to jump to chapters mainActivityAppBar?.height ?: 0
it.post { )
updateToolbarTitleAlpha() mainActivityAppBar?.isLifted = true
} }
} }
@ -279,11 +281,6 @@ class MangaController :
scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> { scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = getMainAppBarHeight() topMargin = getMainAppBarHeight()
} }
scroller.applyInsetter {
type(navigationBars = true) {
margin()
}
}
} }
binding.swipeRefresh.doOnLayout { swipeRefresh -> binding.swipeRefresh.doOnLayout { swipeRefresh ->
@ -299,22 +296,10 @@ class MangaController :
} }
} }
} }
// Tablet layout // Tablet layout
binding.infoRecycler?.let { binding.infoRecycler?.adapter = mangaInfoAdapter
it.adapter = mangaInfoAdapter binding.chaptersRecycler?.adapter = ConcatAdapter(chaptersHeaderAdapter, chaptersAdapter)
it.scrollEvents()
.onEach { updateToolbarTitleAlpha() }
.launchIn(viewScope)
// Delayed in case we need to jump to chapters
it.post {
updateToolbarTitleAlpha()
}
}
binding.chaptersRecycler?.let {
it.adapter = ConcatAdapter(chaptersHeaderAdapter, chaptersAdapter)
}
chaptersAdapter?.fastScroller = binding.fastScroller chaptersAdapter?.fastScroller = binding.fastScroller
@ -339,6 +324,20 @@ class MangaController :
trackSheet = TrackSheet(this, manga!!, (activity as MainActivity).supportFragmentManager) trackSheet = TrackSheet(this, manga!!, (activity as MainActivity).supportFragmentManager)
updateFilterIconState() updateFilterIconState()
recyclerViewUpdatesToolbarTitleAlpha(true)
}
private fun recyclerViewUpdatesToolbarTitleAlpha(enable: Boolean) {
val recycler = binding.fullRecycler ?: binding.infoRecycler ?: return
if (enable) {
if (!recyclerViewToolbarTitleAlphaUpdaterAdded) {
recycler.addOnScrollListener(recyclerViewToolbarTitleAlphaUpdater)
recyclerViewToolbarTitleAlphaUpdaterAdded = true
}
} else if (recyclerViewToolbarTitleAlphaUpdaterAdded) {
recycler.removeOnScrollListener(recyclerViewToolbarTitleAlphaUpdater)
recyclerViewToolbarTitleAlphaUpdaterAdded = false
}
} }
private fun updateToolbarTitleAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float? = null) { private fun updateToolbarTitleAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float? = null) {
@ -399,6 +398,7 @@ class MangaController :
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
recyclerViewUpdatesToolbarTitleAlpha(false)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
binding.actionToolbar.destroy() binding.actionToolbar.destroy()
mangaInfoAdapter = null mangaInfoAdapter = null
@ -579,8 +579,7 @@ class MangaController :
} }
}.toTypedArray() }.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) showChangeCategoryDialog(manga, categories, preselected)
.showDialog(router)
} }
} }
@ -608,6 +607,9 @@ class MangaController :
*/ */
private fun toggleFavorite() { private fun toggleFavorite() {
val isNowFavorite = presenter.toggleFavorite() val isNowFavorite = presenter.toggleFavorite()
if (isNowFavorite) {
addSnackbar?.dismiss()
}
if (activity != null && !isNowFavorite && presenter.hasDownloads()) { if (activity != null && !isNowFavorite && presenter.hasDownloads()) {
(activity as? MainActivity)?.binding?.rootCoordinator?.snack(activity!!.getString(R.string.delete_downloads_for_manga)) { (activity as? MainActivity)?.binding?.rootCoordinator?.snack(activity!!.getString(R.string.delete_downloads_for_manga)) {
setAction(R.string.action_delete) { setAction(R.string.action_delete) {
@ -615,7 +617,6 @@ class MangaController :
} }
} }
} }
mangaInfoAdapter?.notifyDataSetChanged() mangaInfoAdapter?.notifyDataSetChanged()
} }
@ -631,8 +632,21 @@ class MangaController :
QuadStateTextView.State.UNCHECKED.ordinal QuadStateTextView.State.UNCHECKED.ordinal
} }
}.toTypedArray() }.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) showChangeCategoryDialog(manga, categories, preselected)
.showDialog(router) }
private fun showChangeCategoryDialog(manga: Manga, categories: List<Category>, preselected: Array<Int>) {
if (dialog != null) return
dialog = ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
dialog?.addLifecycleListener(
object : LifecycleListener() {
override fun postDestroy(controller: Controller) {
super.postDestroy(controller)
dialog = null
}
}
)
dialog?.showDialog(router)
} }
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
@ -816,7 +830,7 @@ class MangaController :
fun onSetCoverSuccess() { fun onSetCoverSuccess() {
mangaInfoAdapter?.notifyDataSetChanged() mangaInfoAdapter?.notifyDataSetChanged()
dialog?.setImage(manga) (dialog as? MangaFullCoverDialog)?.setImage(manga)
activity?.toast(R.string.cover_updated) activity?.toast(R.string.cover_updated)
} }
@ -1111,7 +1125,7 @@ class MangaController :
val manga = presenter.manga val manga = presenter.manga
presenter.downloadChapters(chapters) presenter.downloadChapters(chapters)
if (view != null && !manga.favorite) { if (view != null && !manga.favorite) {
addSnackbar = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { addSnackbar = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(view.context.getString(R.string.snack_add_to_library)) {
setAction(R.string.action_add) { setAction(R.string.action_add) {
if (!manga.favorite) { if (!manga.favorite) {
addToLibrary(manga) addToLibrary(manga)

View File

@ -365,10 +365,13 @@ class MangaInfoHeaderAdapter(
} }
} }
private fun updateDescription(description: String?, isCurrentlyExpanded: Boolean): CharSequence? { private fun updateDescription(description: String?, isCurrentlyExpanded: Boolean): CharSequence {
return when { return when {
description.isNullOrBlank() -> view.context.getString(R.string.unknown) description.isNullOrBlank() -> view.context.getString(R.string.unknown)
isCurrentlyExpanded -> description.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n") isCurrentlyExpanded ->
description
.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
else -> description else -> description
} }
} }

View File

@ -9,6 +9,8 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.LicensesControllerBinding import eu.kanade.tachiyomi.databinding.LicensesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
class LicensesController : class LicensesController :
@ -30,15 +32,25 @@ class LicensesController :
padding() padding()
} }
} }
binding.progress.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
adapter = LicensesAdapter(this) adapter = LicensesAdapter(this)
binding.recycler.adapter = adapter binding.recycler.adapter = adapter
val licenseItems = Libs(view.context).libraries viewScope.launchUI {
.sortedBy { it.libraryName.lowercase() } val licenseItems = withIOContext {
.map { LicensesItem(it) } Libs(view.context).libraries
adapter?.updateDataSet(licenseItems) .sortedBy { it.libraryName.lowercase() }
.map { LicensesItem(it) }
}
binding.progress.hide()
adapter?.updateDataSet(licenseItems)
}
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {

View File

@ -27,7 +27,6 @@ import android.view.WindowManager
import android.view.animation.Animation import android.view.animation.Animation
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.SeekBar
import android.widget.Toast import android.widget.Toast
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -39,6 +38,7 @@ import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.slider.Slider
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -74,7 +74,6 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.popupMenu import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import eu.kanade.tachiyomi.widget.listener.SimpleSeekBarListener
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@ -85,6 +84,7 @@ import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max
/** /**
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the * Activity containing the reader of Tachiyomi. This activity is mostly a container of the
@ -328,26 +328,22 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
} }
// Init listeners on bottom menu // Init listeners on bottom menu
binding.pageSeekbar.setOnSeekBarChangeListener( binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
object : SimpleSeekBarListener() { override fun onStartTrackingTouch(slider: Slider) {
override fun onStartTrackingTouch(seekBar: SeekBar) { isScrollingThroughPages = true
super.onStartTrackingTouch(seekBar)
isScrollingThroughPages = true
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
super.onStopTrackingTouch(seekBar)
isScrollingThroughPages = false
}
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (viewer != null && fromUser) {
moveToPageIndex(value)
binding.pageSeekbar.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
} }
)
override fun onStopTrackingTouch(slider: Slider) {
isScrollingThroughPages = false
}
})
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
if (viewer != null && fromUser) {
isScrollingThroughPages = true
moveToPageIndex(value.toInt())
slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
binding.leftChapter.setOnClickListener { binding.leftChapter.setOnClickListener {
if (viewer != null) { if (viewer != null) {
if (viewer is R2LPagerViewer) { if (viewer is R2LPagerViewer) {
@ -600,7 +596,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.toolbar.title = manga.title binding.toolbar.title = manga.title
binding.pageSeekbar.isRTL = newViewer is R2LPagerViewer binding.pageSlider.isRTL = newViewer is R2LPagerViewer
if (newViewer is R2LPagerViewer) { if (newViewer is R2LPagerViewer) {
binding.leftChapter.setTooltip(R.string.action_next_chapter) binding.leftChapter.setTooltip(R.string.action_next_chapter)
binding.rightChapter.setTooltip(R.string.action_previous_chapter) binding.rightChapter.setTooltip(R.string.action_previous_chapter)
@ -724,7 +720,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
// Set bottom page number // Set bottom page number
binding.pageNumber.text = "${page.number}/${pages.size}" binding.pageNumber.text = "${page.number}/${pages.size}"
// Set seekbar page number // Set page numbers
if (viewer !is R2LPagerViewer) { if (viewer !is R2LPagerViewer) {
binding.leftPageText.text = "${page.number}" binding.leftPageText.text = "${page.number}"
binding.rightPageText.text = "${pages.size}" binding.rightPageText.text = "${pages.size}"
@ -733,9 +729,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.leftPageText.text = "${pages.size}" binding.leftPageText.text = "${pages.size}"
} }
// Set seekbar progress // Set slider progress
binding.pageSeekbar.max = pages.lastIndex binding.pageSlider.isEnabled = pages.size > 1
binding.pageSeekbar.progress = page.index binding.pageSlider.valueTo = max(pages.lastIndex.toFloat(), 1f)
binding.pageSlider.value = page.index.toFloat()
} }
/** /**

View File

@ -1,55 +0,0 @@
package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatSeekBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getThemeColor
/**
* Seekbar to show current chapter progress.
*/
class ReaderSeekBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppCompatSeekBar(context, attrs) {
/**
* Whether the seekbar should draw from right to left.
*/
var isRTL = false
/**
* Draws the seekbar, translating the canvas if using a right to left reader.
*/
override fun draw(canvas: Canvas) {
if (isRTL) {
val px = width / 2f
val py = height / 2f
canvas.scale(-1f, 1f, px, py)
}
super.draw(canvas)
}
/**
* Handles touch events, translating coordinates if using a right to left reader.
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
if (isRTL) {
event.setLocation(width - event.x, event.y)
}
return super.onTouchEvent(event)
}
init {
// Set color to onPrimary when ColoredBars theme is applied
if (context.getThemeColor(R.attr.colorToolbar) == context.getThemeColor(R.attr.colorPrimary)) {
thumbTintList = ColorStateList.valueOf(context.getThemeColor(R.attr.colorOnPrimary))
progressTintList = thumbTintList
}
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.slider.Slider
/**
* Slider to show current chapter progress.
*/
class ReaderSlider @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : Slider(context, attrs) {
init {
isTickVisible = false
stepSize = 1f
setLabelFormatter { value ->
(value.toInt() + 1).toString()
}
}
/**
* Whether the slider should draw from right to left.
*/
var isRTL: Boolean
set(value) {
layoutDirection = if (value) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR
}
get() = layoutDirection == LAYOUT_DIRECTION_RTL
}

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader.setting
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.SeekBar
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.graphics.alpha import androidx.core.graphics.alpha
import androidx.core.graphics.blue import androidx.core.graphics.blue
@ -15,7 +14,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ReaderColorFilterSettingsBinding import eu.kanade.tachiyomi.databinding.ReaderColorFilterSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.preference.bindToPreference import eu.kanade.tachiyomi.util.preference.bindToPreference
import eu.kanade.tachiyomi.widget.listener.SimpleSeekBarListener
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
@ -54,13 +52,13 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
// Set brightness value // Set brightness value
binding.txtBrightnessSeekbarValue.text = brightness.toString() binding.txtBrightnessSeekbarValue.text = brightness.toString()
binding.brightnessSeekbar.progress = brightness binding.sliderBrightness.value = brightness.toFloat()
// Initialize seekBar progress // Initialize seekBar progress
binding.seekbarColorFilterAlpha.progress = argb[0] binding.sliderColorFilterAlpha.value = argb[0].toFloat()
binding.seekbarColorFilterRed.progress = argb[1] binding.sliderColorFilterRed.value = argb[1].toFloat()
binding.seekbarColorFilterGreen.progress = argb[2] binding.sliderColorFilterGreen.value = argb[2].toFloat()
binding.seekbarColorFilterBlue.progress = argb[3] binding.sliderColorFilterBlue.value = argb[3].toFloat()
// Set listeners // Set listeners
binding.switchColorFilter.bindToPreference(preferences.colorFilter()) binding.switchColorFilter.bindToPreference(preferences.colorFilter())
@ -69,55 +67,32 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
binding.grayscale.bindToPreference(preferences.grayscale()) binding.grayscale.bindToPreference(preferences.grayscale())
binding.invertedColors.bindToPreference(preferences.invertedColors()) binding.invertedColors.bindToPreference(preferences.invertedColors())
binding.seekbarColorFilterAlpha.setOnSeekBarChangeListener( binding.sliderColorFilterAlpha.addOnChangeListener { _, value, fromUser ->
object : SimpleSeekBarListener() { if (fromUser) {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { setColorValue(value.toInt(), ALPHA_MASK, 24)
if (fromUser) {
setColorValue(value, ALPHA_MASK, 24)
}
}
} }
) }
binding.sliderColorFilterRed.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), RED_MASK, 16)
}
}
binding.sliderColorFilterGreen.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), GREEN_MASK, 8)
}
}
binding.sliderColorFilterBlue.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
setColorValue(value.toInt(), BLUE_MASK, 0)
}
}
binding.seekbarColorFilterRed.setOnSeekBarChangeListener( binding.sliderBrightness.addOnChangeListener { _, value, fromUser ->
object : SimpleSeekBarListener() { if (fromUser) {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { preferences.customBrightnessValue().set(value.toInt())
if (fromUser) {
setColorValue(value, RED_MASK, 16)
}
}
} }
) }
binding.seekbarColorFilterGreen.setOnSeekBarChangeListener(
object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
setColorValue(value, GREEN_MASK, 8)
}
}
}
)
binding.seekbarColorFilterBlue.setOnSeekBarChangeListener(
object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
setColorValue(value, BLUE_MASK, 0)
}
}
}
)
binding.brightnessSeekbar.setOnSeekBarChangeListener(
object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
preferences.customBrightnessValue().set(value)
}
}
}
)
} }
/** /**
@ -125,10 +100,10 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
* @param enabled determines if seekBar gets enabled * @param enabled determines if seekBar gets enabled
*/ */
private fun setColorFilterSeekBar(enabled: Boolean) { private fun setColorFilterSeekBar(enabled: Boolean) {
binding.seekbarColorFilterRed.isEnabled = enabled binding.sliderColorFilterRed.isEnabled = enabled
binding.seekbarColorFilterGreen.isEnabled = enabled binding.sliderColorFilterGreen.isEnabled = enabled
binding.seekbarColorFilterBlue.isEnabled = enabled binding.sliderColorFilterBlue.isEnabled = enabled
binding.seekbarColorFilterAlpha.isEnabled = enabled binding.sliderColorFilterAlpha.isEnabled = enabled
} }
/** /**
@ -136,14 +111,14 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
* @param enabled value which determines if seekBar gets enabled * @param enabled value which determines if seekBar gets enabled
*/ */
private fun setCustomBrightnessSeekBar(enabled: Boolean) { private fun setCustomBrightnessSeekBar(enabled: Boolean) {
binding.brightnessSeekbar.isEnabled = enabled binding.sliderBrightness.isEnabled = enabled
} }
/** /**
* Set the text value's of color filter * Set the text value's of color filter
* @param color integer containing color information * @param color integer containing color information
*/ */
fun setValues(color: Int): Array<Int> { private fun setValues(color: Int): Array<Int> {
val alpha = color.alpha val alpha = color.alpha
val red = color.red val red = color.red
val green = color.green val green = color.green
@ -214,21 +189,14 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
* @param mask contains hex mask of chosen color * @param mask contains hex mask of chosen color
* @param bitShift amounts of bits that gets shifted to receive value * @param bitShift amounts of bits that gets shifted to receive value
*/ */
fun setColorValue(color: Int, mask: Long, bitShift: Int) { private fun setColorValue(color: Int, mask: Long, bitShift: Int) {
val currentColor = preferences.colorFilterValue().get() val currentColor = preferences.colorFilterValue().get()
val updatedColor = (color shl bitShift) or (currentColor and mask.inv().toInt()) val updatedColor = (color shl bitShift) or (currentColor and mask.inv().toInt())
preferences.colorFilterValue().set(updatedColor) preferences.colorFilterValue().set(updatedColor)
} }
} }
/** Integer mask of alpha value **/
private const val ALPHA_MASK: Long = 0xFF000000 private const val ALPHA_MASK: Long = 0xFF000000
/** Integer mask of red value **/
private const val RED_MASK: Long = 0x00FF0000 private const val RED_MASK: Long = 0x00FF0000
/** Integer mask of green value **/
private const val GREEN_MASK: Long = 0x0000FF00 private const val GREEN_MASK: Long = 0x0000FF00
/** Integer mask of blue value **/
private const val BLUE_MASK: Long = 0x000000FF private const val BLUE_MASK: Long = 0x000000FF

View File

@ -13,6 +13,8 @@ class UpdatesAdapter(
var readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f) var readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
var unreadColor = context.getResourceColor(R.attr.colorOnSurface) var unreadColor = context.getResourceColor(R.attr.colorOnSurface)
val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
var bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
val coverClickListener: OnCoverClickListener = controller val coverClickListener: OnCoverClickListener = controller

View File

@ -180,7 +180,7 @@ class UpdatesController :
actionMode!!, actionMode!!,
R.menu.updates_chapter_selection R.menu.updates_chapter_selection
) { onActionItemClicked(it!!) } ) { onActionItemClicked(it!!) }
(activity as? MainActivity)?.showBottomNav(visible = false, expand = true) (activity as? MainActivity)?.showBottomNav(false)
} }
toggleSelection(position) toggleSelection(position)
@ -324,6 +324,11 @@ class UpdatesController :
presenter.startDownloadingNow(chapter) presenter.startDownloadingNow(chapter)
} }
private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
presenter.bookmarkChapters(chapters, bookmarked)
destroyActionModeIfNeeded()
}
/** /**
* Called when ActionMode created. * Called when ActionMode created.
* @param mode the ActionMode object * @param mode the ActionMode object
@ -346,6 +351,8 @@ class UpdatesController :
val chapters = getSelectedChapters() val chapters = getSelectedChapters()
binding.actionToolbar.findItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded } binding.actionToolbar.findItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded } binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.bookmark }
binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.bookmark }
binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
} }
@ -370,6 +377,8 @@ class UpdatesController :
R.id.action_delete -> R.id.action_delete ->
ConfirmDeleteChaptersDialog(this, getSelectedChapters()) ConfirmDeleteChaptersDialog(this, getSelectedChapters())
.showDialog(router) .showDialog(router)
R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true)
R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false)
R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
else -> return false else -> return false
@ -386,7 +395,7 @@ class UpdatesController :
adapter?.clearSelection() adapter?.clearSelection()
binding.actionToolbar.hide() binding.actionToolbar.hide()
(activity as? MainActivity)?.showBottomNav(visible = true, expand = true) (activity as? MainActivity)?.showBottomNav(true)
actionMode = null actionMode = null
} }

View File

@ -39,15 +39,20 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
// Set manga title // Set manga title
binding.mangaTitle.text = item.manga.title binding.mangaTitle.text = item.manga.title
// Check if chapter is read and set correct color // Check if chapter is read and/or bookmarked and set correct color
if (item.chapter.read) { if (item.chapter.read) {
binding.chapterTitle.setTextColor(adapter.readColor) binding.chapterTitle.setTextColor(adapter.readColor)
binding.mangaTitle.setTextColor(adapter.readColor) binding.mangaTitle.setTextColor(adapter.readColor)
} else { } else {
binding.chapterTitle.setTextColor(adapter.unreadColor)
binding.mangaTitle.setTextColor(adapter.unreadColor) binding.mangaTitle.setTextColor(adapter.unreadColor)
binding.chapterTitle.setTextColor(
if (item.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary
)
} }
// Set bookmark status
binding.bookmarkIcon.isVisible = item.bookmark
// Set chapter status // Set chapter status
binding.download.isVisible = item.manga.source != LocalSource.ID binding.download.isVisible = item.manga.source != LocalSource.ID
binding.download.setState(item.status, item.progress) binding.download.setState(item.status, item.progress)

View File

@ -180,6 +180,22 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
) )
} }
/**
* Mark selected chapters as bookmarked
* @param items list of selected chapters
* @param bookmarked bookmark status
*/
fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
val chapters = items.map { it.chapter }
chapters.forEach {
it.bookmark = bookmarked
}
Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() }
.subscribeOn(Schedulers.io())
.subscribe()
}
/** /**
* Download selected chapters * Download selected chapters
* @param items list of recent chapters seleted. * @param items list of recent chapters seleted.

View File

@ -36,6 +36,8 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.MiuiUtil
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.isTablet import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -187,6 +189,45 @@ class SettingsAdvancedController : SettingsController() {
} }
} }
preferenceCategory {
titleRes = R.string.label_extensions
listPreference {
key = Keys.extensionInstaller
titleRes = R.string.ext_installer_pref
summary = "%s"
entriesRes = arrayOf(
R.string.ext_installer_legacy,
R.string.ext_installer_packageinstaller,
R.string.ext_installer_shizuku,
)
entryValues = PreferenceValues.ExtensionInstaller.values().map { it.name }.toTypedArray()
defaultValue = if (MiuiUtil.isMiui()) {
PreferenceValues.ExtensionInstaller.LEGACY
} else {
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER
}.name
onChange {
if (it == PreferenceValues.ExtensionInstaller.SHIZUKU.name &&
!context.isPackageInstalled("moe.shizuku.privileged.api")
) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.ext_installer_shizuku)
.setMessage(R.string.ext_installer_shizuku_unavailable_dialog)
.setPositiveButton(android.R.string.ok) { _, _ ->
openInBrowser("https://shizuku.rikka.app/download")
}
.setNegativeButton(android.R.string.cancel, null)
.show()
false
} else {
true
}
}
}
}
preferenceCategory { preferenceCategory {
titleRes = R.string.pref_category_display titleRes = R.string.pref_category_display

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.infoPreference import eu.kanade.tachiyomi.util.preference.infoPreference
@ -11,7 +10,6 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import kotlinx.coroutines.flow.launchIn
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsBrowseController : SettingsController() { class SettingsBrowseController : SettingsController() {
@ -54,18 +52,6 @@ class SettingsBrowseController : SettingsController() {
summaryRes = R.string.requires_app_restart summaryRes = R.string.requires_app_restart
defaultValue = true defaultValue = true
} }
switchPreference {
key = Keys.showNsfwExtension
titleRes = R.string.pref_show_nsfw_extension
defaultValue = true
}
switchPreference {
key = Keys.labelNsfwExtension
titleRes = R.string.pref_label_nsfw_extension
defaultValue = true
preferences.showNsfwExtension().asImmediateFlow { isVisible = it }.launchIn(viewScope)
}
infoPreference(R.string.parental_controls_info) infoPreference(R.string.parental_controls_info)
} }

View File

@ -68,6 +68,9 @@ abstract class SettingsController : PreferenceController() {
animatePreferenceHighlight(it.itemView) animatePreferenceHighlight(it.itemView)
} }
} }
// Explicitly clear it to avoid re-scrolling/animating on activity recreations
preferenceKey = null
} }
} }
} }

View File

@ -282,11 +282,6 @@ class SettingsLibraryController : SettingsController() {
defaultValue = false defaultValue = false
} }
} }
switchPreference {
key = Keys.showLibraryUpdateErrors
titleRes = R.string.pref_library_update_error_notification
defaultValue = true
}
} }
} }

View File

@ -8,6 +8,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
@ -25,7 +26,7 @@ class SettingsSearchController :
/** /**
* Adapter containing search results grouped by lang. * Adapter containing search results grouped by lang.
*/ */
protected var adapter: SettingsSearchAdapter? = null private var adapter: SettingsSearchAdapter? = null
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
init { init {
@ -54,15 +55,18 @@ class SettingsSearchController :
* @param inflater used to load the menu xml. * @param inflater used to load the menu xml.
*/ */
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.settings_main, menu) inflater.inflate(R.menu.settings_main, menu)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Initialize search menu // Initialize search menu
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE searchView.maxWidth = Int.MAX_VALUE
// Change hint to show "search settings."
searchView.queryHint = applicationContext?.getString(R.string.action_search_settings) searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
searchItem.expandActionView() searchItem.expandActionView()
@ -102,8 +106,6 @@ class SettingsSearchController :
super.onViewCreated(view) super.onViewCreated(view)
adapter = SettingsSearchAdapter(this) adapter = SettingsSearchAdapter(this)
// Create recycler and set adapter.
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter binding.recycler.adapter = adapter

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.util.lang
import java.io.Closeable
/**
* Executes the given block function on this resources and then closes it down correctly whether an exception is
* thrown or not.
*
* @param block a function to process with given Closeable resources.
* @return the result of block function invoked on this resource.
*/
inline fun <T : Closeable?> Array<T>.use(block: () -> Unit) {
var blockException: Throwable? = null
try {
return block()
} catch (e: Throwable) {
blockException = e
throw e
} finally {
when (blockException) {
null -> forEach { it?.close() }
else -> forEach {
try {
it?.close()
} catch (closeException: Throwable) {
blockException.addSuppressed(closeException)
}
}
}
}
}

View File

@ -94,6 +94,7 @@ fun initDialog(dialogPreference: DialogPreference) {
inline fun <P : Preference> PreferenceGroup.add(p: P): P { inline fun <P : Preference> PreferenceGroup.add(p: P): P {
return p.apply { return p.apply {
this.isIconSpaceReserved = false this.isIconSpaceReserved = false
this.isSingleLineTitle = false
addPreference(this) addPreference(this)
} }
} }
@ -102,6 +103,7 @@ inline fun <P : Preference> PreferenceGroup.initThenAdd(p: P, block: P.() -> Uni
return p.apply { return p.apply {
block() block()
this.isIconSpaceReserved = false this.isIconSpaceReserved = false
this.isSingleLineTitle = false
addPreference(this) addPreference(this)
} }
} }
@ -109,6 +111,7 @@ inline fun <P : Preference> PreferenceGroup.initThenAdd(p: P, block: P.() -> Uni
inline fun <P : Preference> PreferenceGroup.addThenInit(p: P, block: P.() -> Unit): P { inline fun <P : Preference> PreferenceGroup.addThenInit(p: P, block: P.() -> Unit): P {
return p.apply { return p.apply {
this.isIconSpaceReserved = false this.isIconSpaceReserved = false
this.isSingleLineTitle = false
addPreference(this) addPreference(this)
block() block()
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.content.Context import android.content.Context
import android.view.ViewPropertyAnimator
import android.view.animation.Animation import android.view.animation.Animation
import androidx.constraintlayout.motion.widget.MotionScene.Transition import androidx.constraintlayout.motion.widget.MotionScene.Transition
@ -14,3 +15,8 @@ fun Transition.applySystemAnimatorScale(context: Context) {
// End layout of cover expanding animation tends to break when the transition is less than ~25ms // End layout of cover expanding animation tends to break when the transition is less than ~25ms
this.duration = (this.duration * context.animatorDurationScale).toInt().coerceAtLeast(25) this.duration = (this.duration * context.animatorDurationScale).toInt().coerceAtLeast(25)
} }
/** Scale the duration of this [ViewPropertyAnimator] by [Context.animatorDurationScale] */
fun ViewPropertyAnimator.applySystemAnimatorScale(context: Context): ViewPropertyAnimator = apply {
this.duration = (this.duration * context.animatorDurationScale).toLong()
}

View File

@ -41,6 +41,7 @@ import androidx.core.graphics.green
import androidx.core.graphics.red import androidx.core.graphics.red
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -368,12 +369,51 @@ fun Context.createReaderThemeContext(): Context {
} }
fun Context.isOnline(): Boolean { fun Context.isOnline(): Boolean {
val networkCapabilities = connectivityManager.activeNetwork ?: return false val activeNetwork = connectivityManager.activeNetwork ?: return false
val actNw = connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
val maxTransport = when { val maxTransport = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> NetworkCapabilities.TRANSPORT_WIFI_AWARE Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> NetworkCapabilities.TRANSPORT_WIFI_AWARE
else -> NetworkCapabilities.TRANSPORT_VPN else -> NetworkCapabilities.TRANSPORT_VPN
} }
return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(actNw::hasTransport) return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(networkCapabilities::hasTransport)
}
/**
* Returns true if device is connected to Wifi.
*/
fun Context.isConnectedToWifi(): Boolean {
if (!wifiManager.isWifiEnabled) return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val activeNetwork = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
} else {
@Suppress("DEPRECATION")
wifiManager.connectionInfo.bssid != null
}
}
/**
* Gets document size of provided [Uri]
*
* @return document size of [uri] or null if size can't be obtained
*/
fun Context.getUriSize(uri: Uri): Long? {
return UniFile.fromUri(this, uri).length().takeIf { it >= 0 }
}
/**
* Returns true if [packageName] is installed.
*/
fun Context.isPackageInstalled(packageName: String): Boolean {
return try {
packageManager.getApplicationInfo(packageName, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
} }

View File

@ -49,7 +49,7 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2)
*/ */
inline fun View.snack( inline fun View.snack(
message: String, message: String,
length: Int = Snackbar.LENGTH_LONG, length: Int = 10_000,
f: Snackbar.() -> Unit = {} f: Snackbar.() -> Unit = {}
): Snackbar { ): Snackbar {
val snack = Snackbar.make(this, message, length) val snack = Snackbar.make(this, message, length)

View File

@ -5,10 +5,12 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.TextView import android.widget.TextView
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import com.google.android.material.animation.AnimationUtils import com.google.android.material.animation.AnimationUtils
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.HideToolbarOnScrollBehavior
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.view.findChild import eu.kanade.tachiyomi.util.view.findChild
@ -51,6 +53,8 @@ class ElevationAppBarLayout @JvmOverloads constructor(
} }
} }
override fun getBehavior(): CoordinatorLayout.Behavior<AppBarLayout> = HideToolbarOnScrollBehavior()
/** /**
* Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout] * Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout]
*/ */

View File

@ -1,11 +1,19 @@
package eu.kanade.tachiyomi.widget package eu.kanade.tachiyomi.widget
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.findChild
import kotlin.math.roundToLong
/** /**
* Hide behavior similar to app bar for [BottomNavigationView] * Hide behavior similar to app bar for [BottomNavigationView]
@ -15,6 +23,31 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
attrs: AttributeSet? = null attrs: AttributeSet? = null
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) { ) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
@ViewCompat.NestedScrollType
private var lastStartedType: Int = 0
private var offsetAnimator: ValueAnimator? = null
private var dyRatio = 1F
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
return dependency is AppBarLayout
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: BottomNavigationView,
dependency: View
): Boolean {
val toolbarSize = (dependency as ViewGroup).findChild<Toolbar>()?.height ?: 0
dyRatio = if (toolbarSize > 0) {
child.height.toFloat() / toolbarSize
} else {
1F
}
return false
}
override fun onStartNestedScroll( override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout, coordinatorLayout: CoordinatorLayout,
child: BottomNavigationView, child: BottomNavigationView,
@ -23,7 +56,12 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
axes: Int, axes: Int,
type: Int type: Int
): Boolean { ): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
return false
}
lastStartedType = type
offsetAnimator?.cancel()
return true
} }
override fun onNestedPreScroll( override fun onNestedPreScroll(
@ -36,6 +74,33 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
type: Int type: Int
) { ) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
child.translationY = (child.translationY + dy).coerceIn(0F, child.height.toFloat()) child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
}
override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: BottomNavigationView,
target: View,
type: Int
) {
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
}
}
private fun animateBottomNavigationVisibility(child: BottomNavigationView, isVisible: Boolean) {
offsetAnimator?.cancel()
offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator()
duration = (150 * child.context.animatorDurationScale).roundToLong()
addUpdateListener {
child.translationY = it.animatedValue as Float
}
}
offsetAnimator?.setFloatValues(
child.translationY,
if (isVisible) 0F else child.height.toFloat()
)
offsetAnimator?.start()
} }
} }

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.fastscroller.FastScroller import eu.davidea.fastscroller.FastScroller
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.dpToPxEnd import eu.kanade.tachiyomi.util.system.dpToPxEnd
@ -21,6 +22,12 @@ class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: Attr
) )
autoHideEnabled = true autoHideEnabled = true
ignoreTouchesOutsideHandle = true ignoreTouchesOutsideHandle = true
applyInsetter {
type(navigationBars = true) {
margin()
}
}
} }
// Overridden to handle RTL // Overridden to handle RTL

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import android.widget.SeekBar
import androidx.appcompat.widget.AppCompatSeekBar
import eu.kanade.tachiyomi.R
import kotlin.math.abs
class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
AppCompatSeekBar(context, attrs) {
private var minValue: Int = 0
private var maxValue: Int = 0
private var listener: OnSeekBarChangeListener? = null
init {
val styledAttributes = context.obtainStyledAttributes(
attrs,
R.styleable.NegativeSeekBar,
0,
0
)
try {
setMinSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_min_seek, 0))
setMaxSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_max_seek, 0))
} finally {
styledAttributes.recycle()
}
super.setOnSeekBarChangeListener(
object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, value: Int, fromUser: Boolean) {
listener?.onProgressChanged(seekBar, minValue + value, fromUser)
}
override fun onStartTrackingTouch(p0: SeekBar?) {
listener?.onStartTrackingTouch(p0)
}
override fun onStopTrackingTouch(p0: SeekBar?) {
listener?.onStopTrackingTouch(p0)
}
}
)
}
override fun setProgress(progress: Int) {
super.setProgress(abs(minValue) + progress)
}
fun setMinSeek(minValue: Int) {
this.minValue = minValue
max = (this.maxValue - this.minValue)
}
fun setMaxSeek(maxValue: Int) {
this.maxValue = maxValue
max = (this.maxValue - this.minValue)
}
override fun setOnSeekBarChangeListener(listener: OnSeekBarChangeListener?) {
this.listener = listener
}
override fun onRestoreInstanceState(state: Parcelable?) {
// We can't restore the progress from the saved state because it gets shifted.
val origProgress = progress
super.onRestoreInstanceState(state)
super.setProgress(origProgress)
}
}

View File

@ -8,7 +8,7 @@ import java.util.Stack
abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() { abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
private val pool = Stack<View>() private val pool = HashMap<Int, Stack<View>>()
var recycle = true var recycle = true
set(value) { set(value) {
@ -16,17 +16,20 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
field = value field = value
} }
protected abstract fun createView(container: ViewGroup): View protected abstract fun getViewType(position: Int): Int
protected abstract fun inflateView(container: ViewGroup, viewType: Int): View
protected abstract fun bindView(view: View, position: Int) protected abstract fun bindView(view: View, position: Int)
protected open fun recycleView(view: View, position: Int) {} protected open fun recycleView(view: View, position: Int) {}
override fun createView(container: ViewGroup, position: Int): View { override fun createView(container: ViewGroup, position: Int): View {
val view = if (pool.isNotEmpty()) { val viewType = getViewType(position)
pool.pop().setViewPagerPositionParam(position) val view = if (pool[viewType] != null && pool[viewType]!!.isNotEmpty()) {
pool[viewType]!!.pop().setViewPagerPositionParam(position)
} else { } else {
createView(container) inflateView(container, viewType)
} }
bindView(view, position) bindView(view, position)
return view return view
@ -34,7 +37,9 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
override fun destroyView(container: ViewGroup, position: Int, view: View) { override fun destroyView(container: ViewGroup, position: Int, view: View) {
recycleView(view, position) recycleView(view, position)
if (recycle) pool.push(view) val viewType = getViewType(position)
if (pool[viewType] == null) pool[viewType] = Stack<View>()
if (recycle) pool[viewType]!!.push(view)
} }
/** /**

View File

@ -0,0 +1,174 @@
package eu.kanade.tachiyomi.widget
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.ViewPropertyAnimator
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams
import androidx.customview.view.AbsSavedState
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomnavigation.BottomNavigationView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import kotlinx.coroutines.flow.launchIn
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TachiyomiBottomNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.bottomNavigationStyle,
defStyleRes: Int = R.style.Widget_Design_BottomNavigationView
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) {
private var currentAnimator: ViewPropertyAnimator? = null
private var currentState = STATE_UP
init {
// Hide on scroll
doOnLayout {
findViewTreeLifecycleOwner()?.lifecycleScope?.let { scope ->
Injekt.get<PreferencesHelper>().hideBottomBarOnScroll()
.asImmediateFlow {
updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = if (it) {
HideBottomNavigationOnScrollBehavior()
} else {
null
}
}
}
.launchIn(scope)
}
}
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
return SavedState(superState).also {
it.currentState = currentState
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
doOnNextLayout {
if (state.currentState == STATE_UP) {
slideUp(animate = false)
} else if (state.currentState == STATE_DOWN) {
slideDown(animate = false)
}
}
} else {
super.onRestoreInstanceState(state)
}
}
override fun setTranslationY(translationY: Float) {
// Disallow translation change when state down
if (currentState == STATE_DOWN) return
super.setTranslationY(translationY)
}
/**
* Shows this view up.
*
* @param animate True if slide up should be animated
*/
fun slideUp(animate: Boolean = true) {
currentAnimator?.cancel()
clearAnimation()
currentState = STATE_UP
animateTranslation(
0F,
if (animate) SLIDE_UP_ANIMATION_DURATION else 0,
LinearOutSlowInInterpolator()
)
}
/**
* Hides this view down. [setTranslationY] won't work until [slideUp] is called.
*
* @param animate True if slide down should be animated
*/
fun slideDown(animate: Boolean = true) {
currentAnimator?.cancel()
clearAnimation()
currentState = STATE_DOWN
animateTranslation(
height.toFloat(),
if (animate) SLIDE_DOWN_ANIMATION_DURATION else 0,
FastOutLinearInInterpolator()
)
}
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
currentAnimator = animate()
.translationY(targetY)
.setInterpolator(interpolator)
.setDuration(duration)
.applySystemAnimatorScale(context)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
currentAnimator = null
postInvalidate()
}
})
}
internal class SavedState : AbsSavedState {
var currentState = STATE_UP
constructor(superState: Parcelable) : super(superState)
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
currentState = source.readByte().toInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeByte(currentState.toByte())
}
companion object {
@JvmField
val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> {
override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState {
return SavedState(source, loader)
}
override fun createFromParcel(source: Parcel): SavedState {
return SavedState(source, null)
}
override fun newArray(size: Int): Array<SavedState> {
return newArray(size)
}
}
}
}
companion object {
private const val STATE_DOWN = 1
private const val STATE_UP = 2
private const val SLIDE_UP_ANIMATION_DURATION = 225L
private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.widget
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -39,6 +40,18 @@ class TachiyomiSearchView @JvmOverloads constructor(
}.launchIn(scope!!) }.launchIn(scope!!)
} }
override fun setOnQueryTextListener(listener: OnQueryTextListener?) {
super.setOnQueryTextListener(listener)
val searchAutoComplete: SearchAutoComplete = findViewById(R.id.search_src_text)
searchAutoComplete.setOnEditorActionListener { _, actionID, _ ->
if (actionID == EditorInfo.IME_ACTION_SEARCH || actionID == EditorInfo.IME_NULL) {
clearFocus()
listener?.onQueryTextSubmit(query.toString())
true
} else false
}
}
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
scope?.cancel() scope?.cancel()

View File

@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.widget.listener
import android.widget.SeekBar
open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
}

View File

@ -64,29 +64,31 @@
<TextView <TextView
android:id="@+id/manga_full_title" android:id="@+id/manga_full_title"
style="@style/TextAppearance.Medium.Title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/manga_info_full_title_label" android:text="@string/manga_info_full_title_label"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textIsSelectable="false" /> android:textIsSelectable="false" />
<TextView <TextView
android:id="@+id/manga_author" android:id="@+id/manga_author"
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle2"
android:textColor="?android:attr/textColorSecondary"
android:textAlignment="center" android:textAlignment="center"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Author" /> tools:text="Author" />
<TextView <TextView
android:id="@+id/manga_artist" android:id="@+id/manga_artist"
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:textAppearance="?attr/textAppearanceSubtitle2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Artist" /> tools:text="Artist" />
@ -106,31 +108,34 @@
<TextView <TextView
android:id="@+id/manga_status" android:id="@+id/manga_status"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Status" /> tools:text="Status" />
<TextView <TextView
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:text="•" android:text="•"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<TextView <TextView
android:id="@+id/manga_source" android:id="@+id/manga_source"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Source" /> tools:text="Source" />
@ -194,7 +199,6 @@
<TextView <TextView
android:id="@+id/manga_summary_text" android:id="@+id/manga_summary_text"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -203,6 +207,8 @@
android:ellipsize="end" android:ellipsize="end"
android:focusable="true" android:focusable="true"
android:maxLines="3" android:maxLines="3"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -33,7 +33,7 @@
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.SubHeading" android:textAppearance="?attr/textAppearanceBody2"
tools:text="Category Title" /> tools:text="Category Title" />
</LinearLayout> </LinearLayout>

View File

@ -3,8 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight" android:layout_height="wrap_content"
android:background="@drawable/list_item_selector_background" android:background="@drawable/list_item_selector_background"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingVertical="10dp"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="5dp"> android:paddingEnd="5dp">
@ -12,41 +14,44 @@
android:id="@+id/bookmark_icon" android:id="@+id/bookmark_icon"
android:layout_width="16dp" android:layout_width="16dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:contentDescription="@string/action_filter_bookmarked"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/chapter_title" app:layout_constraintBottom_toBottomOf="@id/chapter_title"
app:layout_constraintEnd_toStartOf="@id/chapter_title" app:layout_constraintEnd_toStartOf="@id/chapter_title"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@+id/chapter_title"
app:srcCompat="@drawable/ic_bookmark_24dp" app:srcCompat="@drawable/ic_bookmark_24dp"
app:tint="?attr/colorAccent" app:tint="?attr/colorAccent"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
android:id="@+id/chapter_title" android:id="@+id/chapter_title"
style="@style/TextAppearance.Regular.Body1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintBottom_toTopOf="@+id/chapter_description"
app:layout_constraintEnd_toStartOf="@+id/download" app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toEndOf="@+id/bookmark_icon" app:layout_constraintStart_toEndOf="@+id/bookmark_icon"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Title" /> tools:text="Title" />
<TextView <TextView
android:id="@+id/chapter_description" android:id="@+id/chapter_description"
style="@style/TextAppearance.Regular.Caption"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginTop="6dp"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBody2"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download" app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chapter_title"
tools:text="22/02/2016 • Scanlator • Page: 45" /> tools:text="22/02/2016 • Scanlator • Page: 45" />
<eu.kanade.tachiyomi.ui.manga.chapter.ChapterDownloadView <eu.kanade.tachiyomi.ui.manga.chapter.ChapterDownloadView

View File

@ -10,7 +10,7 @@
android:id="@+id/description" android:id="@+id/description"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Regular.Body1" /> android:textAppearance="?attr/textAppearanceBody2" />
<CheckBox <CheckBox
android:id="@+id/checkbox_option" android:id="@+id/checkbox_option"

View File

@ -9,20 +9,22 @@
<TextView <TextView
android:id="@+id/text_face" android:id="@+id/text_face"
style="@style/TextAppearance.Medium.Body2.Hint"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textSize="48sp" android:textSize="48sp"
tools:text="-_-" /> tools:text="-_-" />
<TextView <TextView
android:id="@+id/text_label" android:id="@+id/text_label"
style="@style/TextAppearance.Medium.Body2.Hint"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:gravity="center" android:gravity="center"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
tools:text="Label" /> tools:text="Label" />
<LinearLayout <LinearLayout

View File

@ -37,7 +37,7 @@
android:layout_toEndOf="@id/reorder" android:layout_toEndOf="@id/reorder"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Body1" android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintEnd_toStartOf="@+id/download_progress_text" app:layout_constraintEnd_toStartOf="@+id/download_progress_text"
app:layout_constraintStart_toEndOf="@+id/reorder" app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -52,7 +52,8 @@
android:layout_toEndOf="@id/reorder" android:layout_toEndOf="@id/reorder"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption" android:textAppearance="?attr/textAppearanceBody2"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@+id/manga_source" app:layout_constraintEnd_toStartOf="@+id/manga_source"
app:layout_constraintStart_toStartOf="@+id/manga_full_title" app:layout_constraintStart_toStartOf="@+id/manga_full_title"
app:layout_constraintTop_toBottomOf="@+id/manga_full_title" app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
@ -75,7 +76,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toEndOf="@id/manga_full_title" android:layout_toEndOf="@id/manga_full_title"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint" android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/manga_full_title" app:layout_constraintBottom_toBottomOf="@+id/manga_full_title"
app:layout_constraintEnd_toStartOf="@+id/menu" app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintTop_toTopOf="@+id/manga_full_title" app:layout_constraintTop_toTopOf="@+id/manga_full_title"
@ -87,7 +90,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toEndOf="@id/chapter_title" android:layout_toEndOf="@id/chapter_title"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint" android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/chapter_title" app:layout_constraintBottom_toBottomOf="@+id/chapter_title"
app:layout_constraintEnd_toStartOf="@+id/menu" app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintTop_toTopOf="@+id/chapter_title" app:layout_constraintTop_toTopOf="@+id/chapter_title"

View File

@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp" android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:background="@drawable/list_item_selector_background"> android:background="@drawable/list_item_selector_background">
<ImageView <ImageView
@ -21,14 +22,12 @@
<TextView <TextView
android:id="@+id/ext_title" android:id="@+id/ext_title"
style="@style/TextAppearance.Regular"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.SubHeading" android:textAppearance="?attr/textAppearanceBody2"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/lang" app:layout_constraintBottom_toTopOf="@id/lang"
app:layout_constraintEnd_toStartOf="@id/ext_button" app:layout_constraintEnd_toStartOf="@id/ext_button"
app:layout_constraintStart_toEndOf="@id/image" app:layout_constraintStart_toEndOf="@id/image"
@ -38,11 +37,10 @@
<TextView <TextView
android:id="@+id/lang" android:id="@+id/lang"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="1" android:maxLines="1"
android:textSize="12sp" android:textAppearance="?attr/textAppearanceCaption"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/image" app:layout_constraintStart_toEndOf="@id/image"
app:layout_constraintTop_toBottomOf="@+id/ext_title" app:layout_constraintTop_toBottomOf="@+id/ext_title"
@ -51,25 +49,23 @@
<TextView <TextView
android:id="@+id/version" android:id="@+id/version"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:maxLines="1" android:maxLines="1"
android:textSize="12sp" android:textAppearance="?attr/textAppearanceCaption"
app:layout_constraintStart_toEndOf="@id/lang" app:layout_constraintStart_toEndOf="@id/lang"
app:layout_constraintTop_toBottomOf="@+id/ext_title" app:layout_constraintTop_toBottomOf="@+id/ext_title"
tools:text="Version" /> tools:text="Version" />
<TextView <TextView
android:id="@+id/warning" android:id="@+id/warning"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?attr/colorError" android:textColor="?attr/colorError"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@id/version" app:layout_constraintStart_toEndOf="@id/version"
app:layout_constraintTop_toBottomOf="@+id/ext_title" app:layout_constraintTop_toBottomOf="@+id/ext_title"
tools:text="Warning" /> tools:text="Warning" />
@ -79,10 +75,24 @@
style="?attr/borderlessButtonStyle" style="?attr/borderlessButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/cancel_button"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Details" /> tools:text="Details" />
<ImageButton
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@android:string/cancel"
android:padding="12dp"
android:src="@drawable/ic_close_24dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:attr/textColorPrimary"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -36,11 +36,11 @@
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:elevation="3dp" android:elevation="3dp"
android:textAppearance="?attr/textAppearanceSubtitle1"
app:layout_constraintStart_toEndOf="@id/icon" app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Tachiyomi: Extension" /> tools:text="Tachiyomi: Extension" />
@ -53,6 +53,7 @@
android:layout_weight="1" android:layout_weight="1"
android:elevation="3dp" android:elevation="3dp"
android:gravity="center" android:gravity="center"
android:textAppearance="?attr/textAppearanceCaption"
app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title"
tools:text="Version: 1.0.0" /> tools:text="Version: 1.0.0" />
@ -65,6 +66,7 @@
android:layout_weight="1" android:layout_weight="1"
android:elevation="3dp" android:elevation="3dp"
android:gravity="center" android:gravity="center"
android:textAppearance="?attr/textAppearanceCaption"
app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/version" app:layout_constraintTop_toBottomOf="@id/version"
tools:text="Language: English" /> tools:text="Language: English" />
@ -78,6 +80,7 @@
android:elevation="3dp" android:elevation="3dp"
android:gravity="center" android:gravity="center"
android:text="@string/ext_nsfw_warning" android:text="@string/ext_nsfw_warning"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?attr/colorError" android:textColor="?attr/colorError"
android:visibility="gone" android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintStart_toStartOf="@id/title"
@ -91,6 +94,7 @@
android:elevation="3dp" android:elevation="3dp"
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceCaption"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/nsfw" app:layout_constraintTop_toBottomOf="@id/nsfw"
@ -98,11 +102,24 @@
<Button <Button
android:id="@+id/btn_uninstall" android:id="@+id/btn_uninstall"
style="@style/Widget.Tachiyomi.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="4dp"
android:text="@string/ext_uninstall" android:text="@string/ext_uninstall"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_app_info"
app:layout_constraintTop_toBottomOf="@id/pkgname" />
<Button
android:id="@+id/btn_app_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="4dp"
android:text="@string/ext_app_info"
app:layout_constraintStart_toEndOf="@+id/btn_uninstall"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/pkgname" /> app:layout_constraintTop_toBottomOf="@id/pkgname" />

View File

@ -14,10 +14,10 @@
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:textAppearance="?attr/textAppearanceSubtitle2"
app:layout_constraintBottom_toTopOf="@+id/subtitle" app:layout_constraintBottom_toTopOf="@+id/subtitle"
app:layout_constraintEnd_toStartOf="@+id/title_more_icon" app:layout_constraintEnd_toStartOf="@+id/title_more_icon"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -27,11 +27,11 @@
<TextView <TextView
android:id="@+id/subtitle" android:id="@+id/subtitle"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
android:textSize="12sp" android:textSize="12sp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@ -46,7 +46,6 @@
<TextView <TextView
android:id="@+id/favorite_text" android:id="@+id/favorite_text"
style="@style/TextAppearance.Regular.Caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorSecondary" android:background="?attr/colorSecondary"
@ -56,6 +55,7 @@
android:paddingEnd="3dp" android:paddingEnd="3dp"
android:paddingBottom="1dp" android:paddingBottom="1dp"
android:text="@string/in_library" android:text="@string/in_library"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="?attr/colorOnSecondary" android:textColor="?attr/colorOnSecondary"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
@ -66,14 +66,13 @@
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
style="@style/TextAppearance.Regular.Body1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="@font/ptsans_narrow_bold"
android:lineSpacingExtra="-4dp"
android:maxLines="2" android:maxLines="2"
android:padding="4dp" android:padding="4dp"
android:textAppearance="?attr/textAppearanceSubtitle2"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="@+id/card" app:layout_constraintEnd_toEndOf="@+id/card"
app:layout_constraintStart_toStartOf="@+id/card" app:layout_constraintStart_toStartOf="@+id/card"
app:layout_constraintTop_toBottomOf="@+id/card" app:layout_constraintTop_toBottomOf="@+id/card"

View File

@ -42,14 +42,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="2"
android:textAppearance="@style/TextAppearance.Medium" android:textAppearance="?attr/textAppearanceSubtitle2"
tools:text="Title" /> tools:text="Title" />
<TextView <TextView
android:id="@+id/manga_subtitle" android:id="@+id/manga_subtitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
tools:text="Subtitle" /> tools:text="Subtitle" />
</LinearLayout> </LinearLayout>

View File

@ -1,7 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
tools:listitem="@layout/licenses_item" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
tools:listitem="@layout/licenses_item" />
</RelativeLayout>

View File

@ -10,21 +10,25 @@
<TextView <TextView
android:id="@+id/name" android:id="@+id/name"
style="@style/TextAppearance.Regular.Body1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle2"
tools:text="Library name" /> tools:text="Library name" />
<TextView <TextView
android:id="@+id/artifact_id" android:id="@+id/artifact_id"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
tools:text="artifact:id:1.0" /> tools:text="artifact:id:1.0" />
<TextView <TextView
android:id="@+id/license" android:id="@+id/license"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
tools:text="Apache Version 2.0" /> tools:text="Apache Version 2.0" />
</LinearLayout> </LinearLayout>

View File

@ -75,7 +75,7 @@
android:id="@+id/fab_layout" android:id="@+id/fab_layout"
layout="@layout/main_activity_fab" /> layout="@layout/main_activity_fab" />
<com.google.android.material.bottomnavigation.BottomNavigationView <eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
android:id="@+id/bottom_nav" android:id="@+id/bottom_nav"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -13,12 +13,11 @@
<TextView <TextView
android:id="@+id/chapters_label" android:id="@+id/chapters_label"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/chapters" android:text="@string/chapters"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textIsSelectable="false" android:textIsSelectable="false"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_chapters_filter" app:layout_constraintEnd_toStartOf="@+id/btn_chapters_filter"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -70,27 +70,29 @@
<TextView <TextView
android:id="@+id/manga_full_title" android:id="@+id/manga_full_title"
style="@style/TextAppearance.Medium.Title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/manga_info_full_title_label" android:text="@string/manga_info_full_title_label"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textIsSelectable="false" /> android:textIsSelectable="false" />
<TextView <TextView
android:id="@+id/manga_author" android:id="@+id/manga_author"
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceSubtitle2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Author" /> tools:text="Author" />
<TextView <TextView
android:id="@+id/manga_artist" android:id="@+id/manga_artist"
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceSubtitle2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Artist" /> tools:text="Artist" />
@ -111,31 +113,34 @@
<TextView <TextView
android:id="@+id/manga_status" android:id="@+id/manga_status"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Status" /> tools:text="Status" />
<TextView <TextView
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:text="•" android:text="•"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<TextView <TextView
android:id="@+id/manga_source" android:id="@+id/manga_source"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
tools:text="Source" /> tools:text="Source" />
@ -205,7 +210,6 @@
<TextView <TextView
android:id="@+id/manga_summary_text" android:id="@+id/manga_summary_text"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
@ -214,6 +218,8 @@
android:ellipsize="end" android:ellipsize="end"
android:focusable="true" android:focusable="true"
android:maxLines="3" android:maxLines="3"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -21,4 +21,11 @@
app:fastScrollerBubbleEnabled="false" app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" /> tools:visibility="visible" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout> </FrameLayout>

View File

@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall" android:layout_height="?attr/listPreferredItemHeightSmall"
xmlns:tools="http://schemas.android.com/tools"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingStart="?attr/listPreferredItemPaddingStart"
@ -17,6 +18,7 @@
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:maxLines="1" android:maxLines="1"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> android:textAppearance="?attr/textAppearanceBody2"
tools:text="Title" />
</LinearLayout> </LinearLayout>

View File

@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall" android:layout_height="?attr/listPreferredItemHeightSmall"
xmlns:tools="http://schemas.android.com/tools"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:paddingStart="?attr/listPreferredItemPaddingStart" android:paddingStart="?attr/listPreferredItemPaddingStart"
@ -15,6 +16,7 @@
android:drawablePadding="16dp" android:drawablePadding="16dp"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> android:textAppearance="?attr/textAppearanceBody2"
tools:text="Title" />
</LinearLayout> </LinearLayout>

View File

@ -17,7 +17,7 @@
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Medium.SubHeading" android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader"
tools:text="Header" /> tools:text="Header" />
<ImageView <ImageView

View File

@ -17,6 +17,6 @@
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:maxLines="1" android:maxLines="1"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> android:textAppearance="?attr/textAppearanceBody2" />
</LinearLayout> </LinearLayout>

View File

@ -17,6 +17,8 @@
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="?android:attr/textColorSecondary"
tools:text="Filter:" /> tools:text="Filter:" />
<Spinner <Spinner

View File

@ -23,7 +23,7 @@
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="text" android:inputType="text"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> android:textAppearance="?attr/textAppearanceBody2" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

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