Compare commits

...

89 Commits

Author SHA1 Message Date
a1554f81ff Fix cover restoration 2023-08-19 18:02:22 +03:00
4ce7f74b9d Update parsers and gradle 2023-08-19 17:44:16 +03:00
0778f34db7 Limit TrackWorker parallelistm when device in use 2023-08-19 15:59:58 +03:00
bc488a6878 Improve search suggestions 2023-08-19 15:11:22 +03:00
856524c9f8 Revert "Remove WorkManager bug workaround"
This reverts commit af654b8c40.
2023-08-19 13:30:30 +03:00
ppg00
c75160d83c Translated using Weblate (Arabic)
Currently translated at 16.9% (80 of 473 strings)

Translated using Weblate (Arabic)

Currently translated at 28.5% (2 of 7 strings)

Co-authored-by: ppg00 <vx2dsk@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-18 16:59:59 +03:00
d9c826524f Translated using Weblate (Russian)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
Christian Elbrianno
40196205eb Translated using Weblate (Indonesian)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-18 16:59:59 +03:00
Dpper
681ac492d6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
Nayuki
95f4606661 Translated using Weblate (Thai)
Currently translated at 26.6% (126 of 472 strings)

Translated using Weblate (Thai)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/th/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-18 16:59:59 +03:00
gallegonovato
b406834d4a Translated using Weblate (Spanish)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (472 of 472 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
InfinityDouki56
2693ec5335 Translated using Weblate (Filipino)
Currently translated at 90.6% (427 of 471 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
ead8a3d6df Restore broken covers and bookmark previews 2023-08-18 16:52:14 +03:00
df04dcc8a3 Show error on ImageActivity 2023-08-18 15:40:56 +03:00
c103773c19 Fix handling favicon loading errors 2023-08-18 15:19:08 +03:00
0a28f131ee Update strings 2023-08-18 15:12:49 +03:00
c8e4842b6e Unify list spacings 2023-08-18 14:33:30 +03:00
d54d489494 Fixes 2023-08-18 12:59:45 +03:00
c77cb4cb3c Fix sources migration 2023-08-18 10:13:56 +03:00
d951306a90 Fix MangaListActivity header scrim 2023-08-18 10:12:12 +03:00
c871757893 Fix colorBackground attribute 2023-08-18 09:43:26 +03:00
64bf671e8b Merge remote-tracking branch 'origin/devel' into devel 2023-08-18 07:50:00 +03:00
b00bc22ead Update UI 2023-08-18 07:49:29 +03:00
fdea2b47da Fix sources migration 2023-08-17 17:10:41 +03:00
91266183c2 Hide categories notification indicators if tracking disabled 2023-08-17 17:00:48 +03:00
20a7e5a6a8 Handle offline mode in history list 2023-08-15 17:02:39 +03:00
6fa99791b6 Handle manga links 2023-08-15 12:46:46 +03:00
4090c8ad6a Fix crash on bookmarks 2023-08-15 10:14:11 +03:00
c72ed7cc34 Update dependencies and Gradle 2023-08-15 10:04:27 +03:00
925c24471e Upgrade targetSdk to 34 2023-08-14 15:10:08 +03:00
7e31b1384e Option to disable related manga 2023-08-14 14:18:09 +03:00
e1efb5fb1e Fix tests 2023-08-14 13:44:55 +03:00
29b5655efb Update screenshots 2023-08-14 13:15:56 +03:00
68bdd22634 Remove deprecated preferences 2023-08-14 10:19:25 +03:00
2e7867f60c Merge branch 'devel' 2023-08-14 10:09:56 +03:00
e4c2972cae Update parsers 2023-08-14 09:59:00 +03:00
af654b8c40 Remove WorkManager bug workaround 2023-08-14 09:44:56 +03:00
9eace89d4f Update error messages 2023-08-14 09:44:02 +03:00
7dbec8eb8f Update bottom nav item animations 2023-08-13 16:09:43 +03:00
111f816f18 Update themes 2023-08-13 15:59:05 +03:00
d4589716aa Improve tablet layout 2023-08-12 18:20:15 +03:00
1c4bd6da28 Retry tracker after errors 2023-08-12 13:55:13 +03:00
c83538f66d Merge pull request #450 from weblate/weblate-kotatsu-strings 2023-08-12 12:59:38 +03:00
gallegonovato
885cae7635 Translated using Weblate (Arabic)
Currently translated at 16.9% (80 of 471 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (471 of 471 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-12 06:39:19 +02:00
InfinityDouki56
c66d36abf9 Translated using Weblate (Filipino)
Currently translated at 91.0% (428 of 470 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-08-12 06:39:19 +02:00
Макар Разин
4bd9c6df81 Translated using Weblate (Chinese (Simplified))
Currently translated at 91.4% (430 of 470 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-08-12 06:39:18 +02:00
Nayuki
b835cb98b7 Translated using Weblate (Thai)
Currently translated at 25.5% (120 of 470 strings)

Translated using Weblate (Thai)

Currently translated at 20.8% (98 of 470 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-12 06:39:18 +02:00
Balog Ferenc József
7cb1f90155 Translated using Weblate (Hungarian)
Currently translated at 28.5% (2 of 7 strings)

Added translation using Weblate (Hungarian)

Co-authored-by: Balog Ferenc József <ferencb2412@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/hu/
Translation: Kotatsu/plurals
2023-08-12 06:39:17 +02:00
96bac81b84 Fix captcha detection 2023-08-11 15:13:25 +03:00
b59f933031 Fix strings 2023-08-11 14:51:56 +03:00
caebca36de Option to hide nsfw content 2023-08-11 14:50:56 +03:00
03cb458d92 Remove custom activity animation 2023-08-11 14:09:21 +03:00
0788f5f05e Adjust cells content to grid size 2023-08-11 13:58:26 +03:00
0271ed2ba9 Show updated manga on top of feed 2023-08-11 13:18:23 +03:00
788c7b862a Fallback to another favicon on error 2023-08-11 12:30:47 +03:00
f4f84099cc Update parsers 2023-08-11 12:23:08 +03:00
3bea94bf1f Handle 429 TooManyRequests error 2023-08-11 11:21:25 +03:00
746eed698f Refactor tracker 2023-08-11 10:36:58 +03:00
fa0289eb27 Show search results once received 2023-08-10 09:09:04 +03:00
c874d73c04 Fix warnings and enable locales-config auto-generating 2023-08-09 16:17:59 +03:00
edb91c46d4 Option to disable pages animation #406 2023-08-09 14:50:52 +03:00
4b9f4f9af2 GC manga updates 2023-08-09 13:17:13 +03:00
J. Lavoie
a07117087a Translated using Weblate (French)
Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Italian)

Currently translated at 78.9% (371 of 470 strings)

Translated using Weblate (German)

Currently translated at 99.3% (467 of 470 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
kuragehime
ce0ffca197 Translated using Weblate (Japanese)
Currently translated at 100.0% (470 of 470 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
TzurS11
eece4d8f00 Added translation using Weblate (Hebrew)
Co-authored-by: TzurS11 <tzurshafriri11@gmail.com>
2023-08-09 12:27:20 +03:00
Nayuki
419e2e578b Translated using Weblate (Thai)
Currently translated at 11.0% (52 of 470 strings)

Translated using Weblate (Thai)

Currently translated at 71.4% (5 of 7 strings)

Added translation using Weblate (Thai)

Co-authored-by: Nayuki <notkungz.suphakorn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/th/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Cookies
0a7387c22e Translated using Weblate (Vietnamese)
Currently translated at 85.2% (399 of 468 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
InfinityDouki56
2a23c3b3b3 Translated using Weblate (Filipino)
Currently translated at 91.0% (428 of 470 strings)

Translated using Weblate (Filipino)

Currently translated at 91.4% (428 of 468 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
Masowick
0566aa4e6a Translated using Weblate (German)
Currently translated at 98.9% (465 of 470 strings)

Translated using Weblate (German)

Currently translated at 93.8% (441 of 470 strings)

Translated using Weblate (German)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Masowick <Demian@gmx.co.uk>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Макар Разин
3f27acf1aa Translated using Weblate (Ukrainian)
Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (468 of 468 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
JA Sunny
d48c3fbe1b Translated using Weblate (Bengali)
Currently translated at 18.4% (86 of 467 strings)

Translated using Weblate (Bengali)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: JA Sunny <ayasbinshams2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
kuragehime
6720474667 Translated using Weblate (Japanese)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (467 of 467 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Eric
ad42ca5085 Translated using Weblate (Chinese (Simplified))
Currently translated at 92.0% (429 of 466 strings)

Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
gallegonovato
d5e40d79ec Translated using Weblate (Spanish)
Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Макар Разин
7853b9a73e Translated using Weblate (Belarusian)
Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Korean)

Currently translated at 76.3% (356 of 466 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (466 of 466 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (466 of 466 strings)

Translated using Weblate (Japanese)

Currently translated at 96.3% (449 of 466 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (466 of 466 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
return_null
cb71a24d81 Translated using Weblate (Chinese (Simplified))
Currently translated at 95.5% (427 of 447 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
a5d99db105 Use nio for File.listFiles() #449 2023-08-09 12:22:52 +03:00
6cc13784e4 Remove invalid track logs 2023-08-07 17:59:26 +03:00
cf9aab9afe Update parsers 2023-08-06 09:22:47 +03:00
83a919f9f6 Add contribution guidelines 2023-08-06 09:12:51 +03:00
70bf80ad8f Update parsers 2023-08-06 09:11:30 +03:00
05a724eae0 Add contribution guidelines 2023-08-04 17:08:50 +03:00
496f3637c4 Update gradle and kotlin 2023-08-04 16:32:08 +03:00
c5c907c8dc UI improvements 2023-08-02 16:45:01 +03:00
af4845a770 Revert "Use ConnectivityManagerCompat.getRestrictBackgroundStatus()"
This reverts commit bfad632b8c.
2023-08-02 14:55:22 +03:00
511f9af991 Limit pages loading parallelism 2023-08-02 14:55:15 +03:00
baf6f6d2c9 Re-use ZipFile instances in PageLoader 2023-08-02 14:35:55 +03:00
8b6a0a8c87 Improve downloads list 2023-08-02 12:42:57 +03:00
e7ee261680 Grouping history by progress 2023-08-02 12:08:28 +03:00
295 changed files with 5022 additions and 2828 deletions

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
## Kotatsu contribution guidelines
- If you want to fix bug or implement a new feature, that already mention in the [issues](https://github.com/KotatsuApp/Kotatsu/issues), please, assign this issue to you and/or comment about it.
- Whether you have to implement new feature, please, open an issue or discussion regarding it to ensure it will be accepted.
- Translations have to be managed using the [Weblate](https://hosted.weblate.org/engage/kotatsu/) platform.
- In case you want to add a new manga source, refer to the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers).
Refactoring or some dev-faces improvements are also might be accepted, however please stick to the following principles:
- Performance matters. In the case of choosing between source code beauty and performance, performance should be a priority.
- Please, do not modify readme and other information files (except for typos).
- Avoid adding new dependencies unless required. APK size is important.

View File

@@ -39,6 +39,10 @@ Kotatsu is a free and open source manga reader for Android.
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/) please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
### Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
### License ### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -2,30 +2,29 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize' id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin' id 'dagger.hilt.android.plugin'
} }
android { android {
compileSdk = 33 compileSdk = 34
buildToolsVersion = '33.0.2' buildToolsVersion = '34.0.0'
namespace = 'org.koitharu.kotatsu' namespace = 'org.koitharu.kotatsu'
defaultConfig { defaultConfig {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
//TODO: update as soon as sources becomes available targetSdkVersion 34
//noinspection OldTargetApi versionCode 571
targetSdkVersion 33 versionName '6.0-a2'
versionCode 567
versionName '5.3.10'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
kapt { arg("room.schemaLocation", "$projectDir/schemas")
arguments { }
arg 'room.schemaLocation', "$projectDir/schemas".toString() androidResources {
} generateLocaleConfig true
} }
} }
buildTypes { buildTypes {
@@ -41,6 +40,7 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
sourceSets { sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
@@ -81,11 +81,11 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:03b4fc9f00') { implementation('com.github.KotatsuApp:kotatsu-parsers:06a2aa6f97') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
@@ -100,11 +100,10 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.9.0' implementation 'com.google.android.material:material:1.9.0'
//noinspection LifecycleAnnotationProcessorWithJava8 implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.1'
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
// TODO https://issuetracker.google.com/issues/254846063 // TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.8.1'
@@ -117,12 +116,11 @@ dependencies {
implementation 'androidx.room:room-runtime:2.5.2' implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.room:room-ktx:2.5.2' implementation 'androidx.room:room-ktx:2.5.2'
//noinspection KaptUsageInsteadOfKsp ksp 'androidx.room:room-compiler:2.5.2'
kapt 'androidx.room:room-compiler:2.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
implementation 'com.squareup.okio:okio:3.4.0' implementation 'com.squareup.okio:okio:3.5.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
@@ -138,8 +136,8 @@ dependencies {
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
implementation 'ch.acra:acra-http:5.11.0' implementation 'ch.acra:acra-http:5.11.1'
implementation 'ch.acra:acra-dialog:5.11.0' implementation 'ch.acra:acra-dialog:5.11.1'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.core.util
import android.util.Log
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
class LoggingAdapterDataObserver(
private val tag: String,
) : AdapterDataObserver() {
override fun onChanged() {
Log.d(tag, "onChanged()")
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
}
override fun onStateRestorationPolicyChanged() {
Log.d(tag, "onStateRestorationPolicyChanged()")
}
}

View File

@@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
@@ -46,7 +47,6 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:localeConfig="@xml/locales"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@@ -71,6 +71,17 @@
<intent-filter> <intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" /> <action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="kotatsu.app" />
<data android:path="/manga" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity" android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
@@ -166,9 +177,6 @@
<activity <activity
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity" android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
android:label="@string/color_correction" /> android:label="@string/color_correction" />
<activity
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
android:label="@string/settings" />
<activity <activity
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity" android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
android:exported="true" android:exported="true"
@@ -189,7 +197,13 @@
</activity> </activity>
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" /> <service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<service
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
android:foregroundServiceType="dataSync" />
<service <service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
@@ -302,6 +316,660 @@
android:name="com.samsung.android.icon_container.has_icon_container" android:name="com.samsung.android.icon_container.has_icon_container"
android:value="@bool/com_samsung_android_icon_container_has_icon_container" /> android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
<activity-alias
android:name="org.koitharu.kotatsu.details.ui.DetailsBYLinkActivity"
android:exported="true"
android:targetActivity="org.koitharu.kotatsu.details.ui.DetailsActivity">
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="1stkissmanga.me" />
<data android:host="3asq.org" />
<data android:host="18porncomic.com" />
<data android:host="212.32.226.234" />
<data android:host="247manga.com" />
<data android:host="365manga.com" />
<data android:host="2023.allhen.online" />
<data android:host="adultwebtoon.com" />
<data android:host="afroditscans.com" />
<data android:host="ainzscans.site" />
<data android:host="aiyumanga.com" />
<data android:host="alceascan.my.id" />
<data android:host="allporncomic.com" />
<data android:host="anibel.net" />
<data android:host="anigliscans.com" />
<data android:host="anikiga.com" />
<data android:host="animaregia.net" />
<data android:host="anisamanga.com" />
<data android:host="anshscans.org" />
<data android:host="apenasmaisumyaoi.com" />
<data android:host="apollcomics.com" />
<data android:host="aquamanga.com" />
<data android:host="arabtoons.net" />
<data android:host="araznovel.com" />
<data android:host="arcanescans.com" />
<data android:host="arenascans.net" />
<data android:host="arthurscan.xyz" />
<data android:host="astral-manga.fr" />
<data android:host="astrallibrary.net" />
<data android:host="astrumscans.xyz" />
<data android:host="asura.nacm.xyz" />
<data android:host="asurascanstr.com" />
<data android:host="athenafansub.com" />
<data android:host="ayatoon.com" />
<data android:host="azoranov.com" />
<data android:host="azuremanga.com" />
<data android:host="babeltoon.com" />
<data android:host="bakai.org" />
<data android:host="bakaman.net" />
<data android:host="bakamh.com" />
<data android:host="banana-scan.com" />
<data android:host="bato.to" />
<data android:host="batocomic.com" />
<data android:host="batocomic.net" />
<data android:host="batocomic.org" />
<data android:host="batotoo.com" />
<data android:host="batotwo.com" />
<data android:host="battwo.com" />
<data android:host="beast-scans.com" />
<data android:host="beehentai.com" />
<data android:host="bentomanga.com" />
<data android:host="bestmanga.club" />
<data android:host="bestmanhua.com" />
<data android:host="bibimanga.com" />
<data android:host="birdmanga.com" />
<data android:host="birdtoon.net" />
<data android:host="blogmanga.net" />
<data android:host="blogtruyenmoi.com" />
<data android:host="bokugents.com" />
<data android:host="boosei.net" />
<data android:host="boyslove.me" />
<data android:host="br.atlantisscan.com" />
<data android:host="br.ninemanga.com" />
<data android:host="cabaredowatame.site" />
<data android:host="cafecomyaoi.com.br" />
<data android:host="carteldemanhwas.com" />
<data android:host="cat300.com" />
<data android:host="cerisescans.com" />
<data android:host="chap.mangairo.com" />
<data android:host="chapmanganato.com" />
<data android:host="chapmanganato.com" />
<data android:host="cizgiromanarsivi.com" />
<data android:host="cmreader.info" />
<data android:host="cocorip.net" />
<data android:host="coffeemanga.io" />
<data android:host="coloredmanga.com" />
<data android:host="comick.app" />
<data android:host="comiko.net" />
<data android:host="comiko.org" />
<data android:host="copypastescan.xyz" />
<data android:host="cosmicscans.com" />
<data android:host="daprob.com" />
<data android:host="darkscans.com" />
<data android:host="de.ninemanga.com" />
<data android:host="desu.me" />
<data android:host="diamondfansub.com" />
<data android:host="dojing.net" />
<data android:host="dokkomanga.com" />
<data android:host="dokkomanga.com" />
<data android:host="doujin69.com" />
<data android:host="doujindesu.rip" />
<data android:host="doujinhentai.net" />
<data android:host="dragontea.ink" />
<data android:host="dragontranslation.net" />
<data android:host="drakescans.com" />
<data android:host="dto.to" />
<data android:host="duckmanga.com" />
<data android:host="duniakomik.id" />
<data android:host="dynasty-scans.com" />
<data android:host="e-hentai.org" />
<data android:host="elarcpage.com" />
<data android:host="en.leviatanscans.com" />
<data android:host="epsilonscan.fr" />
<data android:host="es.ninemanga.com" />
<data android:host="esomanga.com" />
<data android:host="exhentai.org" />
<data android:host="falconmanga.com" />
<data android:host="fbsquads.com" />
<data android:host="finalscans.com" />
<data android:host="flamescans.org" />
<data android:host="foxwhite.com.br" />
<data android:host="fr-scan.cc" />
<data android:host="fr.ninemanga.com" />
<data android:host="franxxmangas.net" />
<data android:host="freakscans.com" />
<data android:host="freemanga.me" />
<data android:host="freemangatop.com" />
<data android:host="freewebtooncoins.com" />
<data android:host="frscans.com" />
<data android:host="furyosociety.com" />
<data android:host="galaxymanga.org" />
<data android:host="gatemanga.com" />
<data android:host="gdscans.com" />
<data android:host="gekkou.com.br" />
<data android:host="glorymanga.com" />
<data android:host="goldenmanga.top" />
<data android:host="golgebahcesi.com" />
<data android:host="gooffansub.com" />
<data android:host="gourmetscans.net" />
<data android:host="grabber.zone" />
<data android:host="gremorymangas.com" />
<data android:host="guimah.com" />
<data android:host="guncelmanga.net" />
<data android:host="h.mangabat.com" />
<data android:host="hachiraw.com" />
<data android:host="harimanga.com" />
<data android:host="hayalistic.com" />
<data android:host="hensekai.com" />
<data android:host="hentai3z.cc" />
<data android:host="hentai3z.xyz" />
<data android:host="hentai4free.net" />
<data android:host="hentai20.io" />
<data android:host="hentai.gekkouscans.com.br" />
<data android:host="hentai.scantrad-vf.cc" />
<data android:host="hentaichan.live" />
<data android:host="hentaichan.pro" />
<data android:host="hentaicube.net" />
<data android:host="hentailib.me" />
<data android:host="hentaimanga.me" />
<data android:host="hentaiteca.net" />
<data android:host="hentaivn.autos" />
<data android:host="hentaivn.tv" />
<data android:host="hentaiwebtoon.com" />
<data android:host="hentaixcomic.com" />
<data android:host="hentaixdickgirl.com" />
<data android:host="hentaixyuri.com" />
<data android:host="hentaizone.xyz" />
<data android:host="herenscan.com" />
<data android:host="hhentai.fr" />
<data android:host="hikariscan.com.br" />
<data android:host="hipercool.xyz" />
<data android:host="hmanhwa.com" />
<data android:host="hni-scantrad.com" />
<data android:host="honey-manga.com.ua" />
<data android:host="hscans.com" />
<data android:host="hto.to" />
<data android:host="id.gourmetscans.net" />
<data android:host="illusionscan.com" />
<data android:host="immortalupdates.com" />
<data android:host="immortalupdates.id" />
<data android:host="imperiodabritannia.com" />
<data android:host="imperioscans.com.br" />
<data android:host="indo18h.com" />
<data android:host="infrafandub.xyz" />
<data android:host="isekaiscan.top" />
<data android:host="it.ninemanga.com" />
<data android:host="itsyourightmanhua.com" />
<data android:host="jaiminisbox.net" />
<data android:host="japscan.ws" />
<data android:host="jiangzaitoon.co" />
<data android:host="jimanga.com" />
<data android:host="jpmangas.xyz" />
<data android:host="kanzenin.xyz" />
<data android:host="karatcam-scans.fr" />
<data android:host="katakomik.online" />
<data android:host="kiryuu.id" />
<data android:host="kissmanga.in" />
<data android:host="klikmanga.id" />
<data android:host="klz9.com" />
<data android:host="koinoboriscan.com" />
<data android:host="kolmanga.com" />
<data android:host="komikav.com" />
<data android:host="komikcast.io" />
<data android:host="komikdewasa.cfd" />
<data android:host="komikgo.org" />
<data android:host="komikhentai.co" />
<data android:host="komikid.com" />
<data android:host="komikindo.co" />
<data android:host="komikindo.info" />
<data android:host="komiklab.com" />
<data android:host="komiklokal.cfd" />
<data android:host="komikmama.co" />
<data android:host="komikmanhwa.me" />
<data android:host="komikmirror.art" />
<data android:host="komiksan.link" />
<data android:host="komiksay.site" />
<data android:host="komikstation.co" />
<data android:host="komiktap.in" />
<data android:host="komiku.com" />
<data android:host="komikzoid.xyz" />
<data android:host="ksgroupscans.com" />
<data android:host="kumascans.com" />
<data android:host="kunmanga.com" />
<data android:host="ladymanga.com" />
<data android:host="lectortmo.com" />
<data android:host="lectorunitoon.com" />
<data android:host="legacy-scans.com" />
<data android:host="legionscans.com" />
<data android:host="leitor.kamisama.com.br" />
<data android:host="leitorizakaya.net" />
<data android:host="lelscanvf.cc" />
<data android:host="leryaoi.com" />
<data android:host="lilymanga.net" />
<data android:host="limascans.xyz/v2" />
<data android:host="lkscanlation.com" />
<data android:host="lolicon.mobi" />
<data android:host="lugnica-scans.com" />
<data android:host="lunarscan.org" />
<data android:host="luxmanga.net" />
<data android:host="lxmanga.net" />
<data android:host="lynxscans.com" />
<data android:host="m.isekaiscan.to" />
<data android:host="mafia-manga.com" />
<data android:host="maidscan.com.br" />
<data android:host="manga1st.online" />
<data android:host="manga3s.com" />
<data android:host="manga18.club" />
<data android:host="manga68.com" />
<data android:host="manga689.com" />
<data android:host="manga-chan.me" />
<data android:host="manga-crab.com" />
<data android:host="manga-diyari.com" />
<data android:host="manga-fast.com" />
<data android:host="manga-fr.me" />
<data android:host="manga-mate.org" />
<data android:host="manga-moons.net" />
<data android:host="manga-scan.co" />
<data android:host="manga-scantrad.io" />
<data android:host="manga-tx.com" />
<data android:host="manga-uptocats.com" />
<data android:host="manga.clone-army.org" />
<data android:host="manga.in.ua" />
<data android:host="manga.mundodrama.site" />
<data android:host="mangaaction.com" />
<data android:host="mangaatrend.net" />
<data android:host="mangabaz.net" />
<data android:host="mangabob.com" />
<data android:host="mangabuddy.com" />
<data android:host="mangacim.com" />
<data android:host="mangaclash.com" />
<data android:host="mangacultivator.com" />
<data android:host="mangacute.com" />
<data android:host="mangacv.com" />
<data android:host="mangadass.com" />
<data android:host="mangadeemak.com" />
<data android:host="mangadex.org" />
<data android:host="mangadistrict.com" />
<data android:host="mangadna.com" />
<data android:host="mangadoor.com" />
<data android:host="mangaeffect.com" />
<data android:host="mangaforest.me" />
<data android:host="mangaforfree.com" />
<data android:host="mangafoxfull.com" />
<data android:host="mangafreak.online" />
<data android:host="mangagalaxy.me" />
<data android:host="mangagg.com" />
<data android:host="mangagoyaoi.com" />
<data android:host="mangagreat.com" />
<data android:host="mangahentai.me" />
<data android:host="mangahub.fr" />
<data android:host="mangaid.click" />
<data android:host="mangaindo.me" />
<data android:host="mangak2.com" />
<data android:host="mangakakalot.com" />
<data android:host="mangakeyfi.net" />
<data android:host="mangaking.net" />
<data android:host="mangakio.me" />
<data android:host="mangakiss.org" />
<data android:host="mangakita.net" />
<data android:host="mangakomi.io" />
<data android:host="mangakyo.org" />
<data android:host="mangalek.com" />
<data android:host="mangaleks.com" />
<data android:host="mangaleveling.com" />
<data android:host="mangalib.me" />
<data android:host="mangalike.me" />
<data android:host="mangalink.online" />
<data android:host="mangalionz.com" />
<data android:host="mangamammy.ru" />
<data android:host="mangamanhua.online" />
<data android:host="mangamaniacs.org" />
<data android:host="manganato.com" />
<data android:host="mangaokutr.com" />
<data android:host="mangaonelove.site" />
<data android:host="mangaonlineteam.com" />
<data android:host="mangaowl.to" />
<data android:host="mangaprotm.com" />
<data android:host="mangapt.com" />
<data android:host="mangapuma.com" />
<data android:host="mangaread.co" />
<data android:host="mangareaderpro.com" />
<data android:host="mangareading.org" />
<data android:host="mangarockteam.com" />
<data android:host="mangarocky.com" />
<data android:host="mangarolls.net" />
<data android:host="mangarosie.in" />
<data android:host="mangas-origines.fr" />
<data android:host="mangas-origines.xyz" />
<data android:host="mangaschan.com" />
<data android:host="mangasehri.com" />
<data android:host="mangaspark.com" />
<data android:host="mangastarz.com" />
<data android:host="mangastic.cc" />
<data android:host="mangastic.cc" />
<data android:host="mangasushi.org" />
<data android:host="mangasusuku.xyz" />
<data android:host="mangatale.co" />
<data android:host="mangatone.com" />
<data android:host="mangatoto.com" />
<data android:host="mangatoto.net" />
<data android:host="mangatoto.org" />
<data android:host="mangatx.com" />
<data android:host="mangaus.xyz" />
<data android:host="mangavisa.com" />
<data android:host="mangaweebs.in" />
<data android:host="mangawt.com" />
<data android:host="mangax1.com" />
<data android:host="mangaxyz.com" />
<data android:host="mangayaro.net" />
<data android:host="mangazavr.ru" />
<data android:host="mangazodiac.com" />
<data android:host="manhatic.com" />
<data android:host="manhuaes.com" />
<data android:host="manhuafast.com" />
<data android:host="manhuafast.net" />
<data android:host="manhuaga.com" />
<data android:host="manhuahot.com" />
<data android:host="manhuamix.com" />
<data android:host="manhuaplus.com" />
<data android:host="manhuascan.us" />
<data android:host="manhuaus.com" />
<data android:host="manhuazone.net" />
<data android:host="manhwa18.app" />
<data android:host="manhwa18.com" />
<data android:host="manhwa18.net" />
<data android:host="manhwa18.org" />
<data android:host="manhwa68.com" />
<data android:host="manhwa-latino.com" />
<data android:host="manhwaclan.com" />
<data android:host="manhwadesu.top" />
<data android:host="manhwafull.com" />
<data android:host="manhwahentai.me" />
<data android:host="manhwaindo.icu" />
<data android:host="manhwaindo.id" />
<data android:host="manhwakool.com" />
<data android:host="manhwalist.xyz" />
<data android:host="manhwalover.com" />
<data android:host="manhwaplus.pro" />
<data android:host="manhwasco.net" />
<data android:host="manhwatop.com" />
<data android:host="manhwaworld.com" />
<data android:host="manhwax.org" />
<data android:host="manhwaz.com" />
<data android:host="mantrazscan.com" />
<data android:host="manwe.pro" />
<data android:host="manycomic.com" />
<data android:host="manytoon.com" />
<data android:host="manytoon.me" />
<data android:host="masterkomik.com" />
<data android:host="melokomik.xyz" />
<data android:host="mgkomik.com" />
<data android:host="miauscans.com" />
<data android:host="milftoon.xxx" />
<data android:host="mintmanga.com" />
<data android:host="mintmanga.live" />
<data android:host="mirrordesu.ink" />
<data android:host="mm-scans.org" />
<data android:host="momonohanascan.com" />
<data android:host="monarcamanga.com" />
<data android:host="moonloversscan.com.br" />
<data android:host="moonwitchinlovescan.com" />
<data android:host="mortalsgroove.com" />
<data android:host="mto.to" />
<data android:host="mundomangakun.com.br" />
<data android:host="mundomanhwa.com" />
<data android:host="murimscan.run" />
<data android:host="neatmangas.com" />
<data android:host="neoxscans.net" />
<data android:host="nettruyenin.com" />
<data android:host="nettruyento.com" />
<data android:host="neumanga.net" />
<data android:host="neumanga.xyz" />
<data android:host="nhattruyenmin.com" />
<data android:host="nhentai.net" />
<data android:host="nicovideo.jp" />
<data android:host="nightscans.org" />
<data android:host="niji-translations.com" />
<data android:host="ninjascan.site" />
<data android:host="niverafansub.com" />
<data android:host="nocsummer.com.br" />
<data android:host="noindexscan.com" />
<data android:host="nonbiri.space" />
<data android:host="novelcrow.com" />
<data android:host="novelmic.com" />
<data android:host="novelstown.cyou" />
<data android:host="nude-moon.net" />
<data android:host="nude-moon.org" />
<data android:host="nyxmanga.com" />
<data android:host="origami-orpheans.com.br" />
<data android:host="otsugami.id" />
<data android:host="oxapk.com" />
<data android:host="ozulmanga.com" />
<data android:host="painfulnightz.com" />
<data android:host="pantheon-scan.com" />
<data android:host="papscan.com" />
<data android:host="paragonscans.com" />
<data android:host="peacescans.com" />
<data android:host="phantomscans.com" />
<data android:host="phenixscans.fr" />
<data android:host="pianmanga.me" />
<data android:host="pirulitorosa.site" />
<data android:host="piscans.in" />
<data android:host="platinumscans.com" />
<data android:host="pojokmanga.net" />
<data android:host="popsmanga.com" />
<data android:host="portalyaoi.com" />
<data android:host="prismahentai.com" />
<data android:host="prismascans.net" />
<data android:host="projetoscanlator.com" />
<data android:host="psunicorn.com" />
<data android:host="queenscans.com" />
<data android:host="ragnarokscan.com" />
<data android:host="ragnarokscanlation.com" />
<data android:host="raijinscans.fr" />
<data android:host="raikiscan.com" />
<data android:host="rainbowfairyscan.com" />
<data android:host="randomscans.com" />
<data android:host="ravenscans.com" />
<data android:host="rawdex.net" />
<data android:host="rawkuma.com" />
<data android:host="read-nifteam.info" />
<data android:host="read.babelwuxia.com" />
<data android:host="readcomicsonline.ru" />
<data android:host="reader.deathtollscans.net" />
<data android:host="reader.decadencescans.com" />
<data android:host="reader.evilflowers.com" />
<data android:host="reader.mangatellers.gr" />
<data android:host="reader.onepiecenakama.pl" />
<data android:host="reader.powermanga.org" />
<data android:host="reader.silentsky-scans.net" />
<data android:host="readfreecomics.com" />
<data android:host="readkomik.com" />
<data android:host="readmanga.io" />
<data android:host="readmanga.live" />
<data android:host="readmanga.me" />
<data android:host="readmangabat.com" />
<data android:host="readmanhua.net" />
<data android:host="readtoto.com" />
<data android:host="readtoto.net" />
<data android:host="readtoto.org" />
<data android:host="realmscans.xyz" />
<data android:host="reaperscans.fr" />
<data android:host="remanga.org" />
<data android:host="rightdark-scan.com" />
<data android:host="rio2manga.com" />
<data android:host="rio2manga.net" />
<data android:host="rogmangas.com" />
<data android:host="romantikmanga.com" />
<data android:host="ru.ninemanga.com" />
<data android:host="s2manga.com" />
<data android:host="samuraiscan.com" />
<data android:host="sawamics.com" />
<data android:host="saytruyenhay.com" />
<data android:host="scambertraslator.com" />
<data android:host="scan.hentai.menu" />
<data android:host="scanmanga-vf.ws" />
<data android:host="scansmangas.me" />
<data android:host="scansraw.com" />
<data android:host="scantrad-union.com" />
<data android:host="scantrad-vf.co" />
<data android:host="sekaikomik.pro" />
<data android:host="sektedoujin.cc" />
<data android:host="sektekomik.xyz" />
<data android:host="selfmanga.live" />
<data android:host="senpaiediciones.com" />
<data android:host="shadowmangas.com" />
<data android:host="shadowtrad.net" />
<data android:host="sheakomik.com" />
<data android:host="shibamanga.com" />
<data android:host="shinigami.id" />
<data android:host="shirodoujin.com" />
<data android:host="shootingstarscans.com" />
<data android:host="silencescan.com.br" />
<data android:host="sinensisscans.com" />
<data android:host="skanlacje-feniksy.pl" />
<data android:host="skymanga.work" />
<data android:host="skymangas.com" />
<data android:host="sleepytranslations.com" />
<data android:host="soulscans.my.id" />
<data android:host="spartanmanga.com.tr" />
<data android:host="sssscanlator.com" />
<data android:host="summanga.com" />
<data android:host="suryascans.com" />
<data android:host="sushiscan.fr" />
<data android:host="sushiscan.net" />
<data android:host="swatop.club" />
<data android:host="tankouhentai.com" />
<data android:host="tatakaescan.com" />
<data android:host="tecnoscann.com" />
<data android:host="teenmanhua.com" />
<data android:host="tempestfansub.com" />
<data android:host="templescan.net" />
<data android:host="templescanesp.com" />
<data android:host="tenkaiscan.net" />
<data android:host="theguildscans.com" />
<data android:host="thesugarscan.com" />
<data android:host="timenaight.com" />
<data android:host="todaymic.com" />
<data android:host="tonizutoon.com" />
<data android:host="toonchill.com" />
<data android:host="toonfr.com" />
<data android:host="toonhunter.com" />
<data android:host="toonily.com" />
<data android:host="toonily.me" />
<data android:host="toonily.net" />
<data android:host="toonitube.com" />
<data android:host="tortuga-ceviri.com" />
<data android:host="traduccionesmoonlight.com" />
<data android:host="treemanga.com" />
<data android:host="tritinia.org" />
<data android:host="truemanga.com" />
<data android:host="truyentranhlh.net" />
<data android:host="tsundoku.com.br" />
<data android:host="tukangkomik.id" />
<data android:host="tumanhwas.club" />
<data android:host="turktoon.com" />
<data android:host="v2.comiz.net" />
<data android:host="valkyriescan.com" />
<data android:host="vercomicsporno.com" />
<data android:host="vermangasporno.com" />
<data android:host="vermanhwa.es" />
<data android:host="viyafansub.com" />
<data android:host="void-scans.com" />
<data android:host="w.mangairo.com" />
<data android:host="wakamics.net" />
<data android:host="webcomic.me" />
<data android:host="webtoon-tr.com" />
<data android:host="webtoon.uk" />
<data android:host="webtoonempire.org" />
<data android:host="webtoonhatti.com" />
<data android:host="webtoons.top" />
<data android:host="webtoonscan.com" />
<data android:host="weloma.art" />
<data android:host="welovemanga.one" />
<data android:host="westmanga.info" />
<data android:host="wickedwitchscan.com" />
<data android:host="winterscan.com" />
<data android:host="wonderlandscan.com" />
<data android:host="woopread.com" />
<data android:host="worldmanhwas.bar" />
<data android:host="wto.to" />
<data android:host="www1.bluesolo.org" />
<data android:host="www.areascans.net" />
<data android:host="www.bentomanga.com" />
<data android:host="www.eromiau.com" />
<data android:host="www.inu-manga.com" />
<data android:host="www.japscan.lol" />
<data android:host="www.kuroimanga.com" />
<data android:host="www.lami-manga.com" />
<data android:host="www.lelmanga.com" />
<data android:host="www.lianscans.my.id" />
<data android:host="www.maid.my.id" />
<data android:host="www.majorscans.com" />
<data android:host="www.mangadods.com" />
<data android:host="www.mangaread.org" />
<data android:host="www.mangascantrad.fr" />
<data android:host="www.mangatown.com" />
<data android:host="www.manhuabug.com" />
<data android:host="www.manhuakey.com" />
<data android:host="www.manhuasy.com" />
<data android:host="www.menudo-fansub.com" />
<data android:host="www.nettruyenmax.com" />
<data android:host="www.nettruyento.com" />
<data android:host="www.nightcomic.com" />
<data android:host="www.ninemanga.com" />
<data android:host="www.noblessetranslations.com" />
<data android:host="www.pantheon-scan.fr" />
<data android:host="www.paritehaber.com" />
<data android:host="www.peachscan.com" />
<data android:host="www.petrotechsociety.org" />
<data android:host="www.petrotechsociety.org" />
<data android:host="www.ramareader.it" />
<data android:host="www.rh2plusmanga.com" />
<data android:host="www.ruyamanga.com" />
<data android:host="www.scan-fr.org" />
<data android:host="www.scan-vf.net" />
<data android:host="www.thaimanga.net" />
<data android:host="www.topmanhua.com" />
<data android:host="www.vfscan.com" />
<data android:host="www.walpurgiscan.it" />
<data android:host="www.webtoon.xyz" />
<data android:host="www.witcomics.net" />
<data android:host="www.xn--l3c0azab5a2gta.com" />
<data android:host="www.yaoitoshokan.net" />
<data android:host="xbato.com" />
<data android:host="xbato.net" />
<data android:host="xbato.org" />
<data android:host="xoxocomics.net" />
<data android:host="xx.hentaichan.live" />
<data android:host="xxx.hentaichan.live" />
<data android:host="y.hentaichan.live" />
<data android:host="yaoi-chan.me" />
<data android:host="yaoi.mobi" />
<data android:host="yaoilib.me" />
<data android:host="yaoiscan.com" />
<data android:host="ycscan.com" />
<data android:host="yugenmangas.com.br" />
<data android:host="yuri.live" />
<data android:host="zahard.xyz" />
<data android:host="zandynofansub.aishiteru.org" />
<data android:host="zbato.com" />
<data android:host="zbato.net" />
<data android:host="zbato.org" />
<data android:host="zeroscan.com.br" />
<data android:host="zinmanga.com" />
<data android:host="zinmanhwa.com" />
<data android:host="zuttomanga.com" />
<data android:host="реманга.орг" />
</intent-filter>
</activity-alias>
</application> </application>
</manifest> </manifest>

View File

@@ -52,6 +52,13 @@ class BookmarksRepository @Inject constructor(
} }
} }
suspend fun updateBookmark(bookmark: Bookmark, imageUrl: String) {
val entity = bookmark.toEntity().copy(
imageUrl = imageUrl,
)
db.bookmarksDao.upsert(listOf(entity))
}
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) { suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) { check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
"Bookmark not found" "Bookmark not found"

View File

@@ -29,7 +29,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.reverseAsync import org.koitharu.kotatsu.core.ui.util.reverseAsync
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
@@ -84,7 +83,7 @@ class BookmarksFragment :
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
val spanResolver = MangaListSpanResolver(resources) val spanResolver = MangaListSpanResolver(resources)
addItemDecoration(TypedListSpacingDecoration(context)) addItemDecoration(TypedListSpacingDecoration(context, false))
adapter = bookmarksAdapter adapter = bookmarksAdapter
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
spanResolver.setGridSize(settings.gridSize / 100f, this) spanResolver.setGridSize(settings.gridSize / 100f, this)
@@ -112,7 +111,7 @@ class BookmarksFragment :
.bookmark(item) .bookmark(item)
.incognito(true) .incognito(true)
.build() .build()
startActivity(intent, scaleUpActivityOptionsOf(view)) startActivity(intent)
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
} }
} }
@@ -159,10 +158,11 @@ class BookmarksFragment :
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
requireViewBinding().recyclerView.updatePadding( val rv = requireViewBinding().recyclerView
bottom = insets.bottom, rv.updatePadding(
bottom = insets.bottom + rv.paddingTop,
) )
requireViewBinding().recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> { rv.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom bottomMargin = insets.bottom
} }
} }

View File

@@ -34,6 +34,7 @@ fun bookmarkListAD(
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
tag(item)
decodeRegion(item.scroll) decodeRegion(item.scroll)
source(item.manga.source) source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)

View File

@@ -5,11 +5,15 @@ import coil.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
class BookmarksAdapter( class BookmarksAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>, clickListener: OnListItemClickListener<Bookmark>,
) : BaseListAdapter<Bookmark>( ) : BaseListAdapter<Bookmark>() {
bookmarkListAD(coil, lifecycleOwner, clickListener),
) init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener))
}
}

View File

@@ -35,6 +35,7 @@ fun bookmarkLargeAD(
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
tag(item)
decodeRegion(item.scroll) decodeRegion(item.scroll)
source(item.manga.source) source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
@@ -27,6 +28,7 @@ class BookmarksAdapter(
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener)) addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
} }
override fun getSectionText(context: Context, position: Int): CharSequence? { override fun getSectionText(context: Context, position: Int): CharSequence? {

View File

@@ -22,7 +22,6 @@ import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.plus import org.koitharu.kotatsu.core.util.ext.plus
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetPagesBinding import org.koitharu.kotatsu.databinding.SheetPagesBinding
@@ -76,7 +75,7 @@ class BookmarksSheet :
) )
viewBinding?.headerBar?.setTitle(R.string.bookmarks) viewBinding?.headerBar?.setTitle(R.string.bookmarks)
with(binding.recyclerView) { with(binding.recyclerView) {
addItemDecoration(TypedListSpacingDecoration(context)) addItemDecoration(TypedListSpacingDecoration(context, false))
adapter = bookmarksAdapter adapter = bookmarksAdapter
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
spanResolver?.setGridSize(settings.gridSize / 100f, this) spanResolver?.setGridSize(settings.gridSize / 100f, this)
@@ -103,7 +102,7 @@ class BookmarksSheet :
.bookmark(item) .bookmark(item)
.incognito(true) .incognito(true)
.build() .build()
startActivity(intent, scaleUpActivityOptionsOf(view)) startActivity(intent)
} }
dismiss() dismiss()
} }

View File

@@ -13,7 +13,6 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding

View File

@@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -18,7 +17,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult import org.koitharu.kotatsu.core.util.TaggedActivityResult

View File

@@ -13,6 +13,7 @@ import coil.decode.SvgDecoder
import coil.disk.DiskCache import coil.disk.DiskCache
import coil.util.DebugLogger import coil.util.DebugLogger
import dagger.Binds import dagger.Binds
import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -46,6 +47,7 @@ import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestorer
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
@@ -89,6 +91,7 @@ interface AppModule {
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor, imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory, pageFetcherFactory: MangaPageFetcher.Factory,
coverRestorerProvider: Lazy<CoverRestorer>,
): ImageLoader { ): ImageLoader {
val diskCacheFactory = { val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir val rootDir = context.externalCacheDir ?: context.cacheDir
@@ -105,6 +108,7 @@ interface AppModule {
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice()) .allowRgb565(context.isLowRamDevice())
.eventListenerFactory { coverRestorerProvider.get() }
.components( .components(
ComponentRegistry.Builder() ComponentRegistry.Builder()
.add(SvgDecoder.Factory()) .add(SvgDecoder.Factory())

View File

@@ -19,6 +19,10 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE manga_id = :id") @Query("SELECT * FROM manga WHERE manga_id = :id")
abstract suspend fun find(id: Long): MangaWithTags? abstract suspend fun find(id: Long): MangaWithTags?
@Transaction
@Query("SELECT * FROM manga WHERE public_url = :publicUrl")
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?
@Transaction @Transaction
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit") @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags> abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>

View File

@@ -15,19 +15,24 @@ class Migration16To17(context: Context) : Migration(16, 17) {
database.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)") database.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty() val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty() val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
val sources = MangaSource.values() val sources = MangaSource.entries
for (source in sources) { for (source in sources) {
if (source == MangaSource.LOCAL) { if (source == MangaSource.LOCAL) {
continue continue
} }
val name = source.name val name = source.name
val isHidden = name in hiddenSources
var sortKey = order.indexOf(name) var sortKey = order.indexOf(name)
if (sortKey == -1) { if (sortKey == -1) {
sortKey = order.size + source.ordinal if (isHidden) {
sortKey = order.size + source.ordinal
} else {
continue
}
} }
database.execSQL( database.execSQL(
"INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)", "INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)",
arrayOf(name, (name !in hiddenSources).toInt(), sortKey), arrayOf(name, (!isHidden).toInt(), sortKey),
) )
} }
} }

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
import java.util.Date
class TooManyRequestExceptions(
val url: String,
val retryAt: Date?,
) : IOException()

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.core.fs
import android.os.Build
import org.koitharu.kotatsu.core.util.iterator.CloseableIterator
import org.koitharu.kotatsu.core.util.iterator.MappingIterator
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
class FileSequence(private val dir: File) : Sequence<File> {
override fun iterator(): Iterator<File> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val stream = Files.newDirectoryStream(dir.toPath())
CloseableIterator(MappingIterator(stream.iterator(), Path::toFile), stream)
} else {
dir.listFiles().orEmpty().iterator()
}
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.model package org.koitharu.kotatsu.core.model
import android.net.Uri
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.core.util.ext.iterator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -66,3 +67,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
val Manga.isLocal: Boolean val Manga.isLocal: Boolean
get() = source == MangaSource.LOCAL get() = source == MangaSource.LOCAL
val Manga.appUrl: Uri
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
.appendQueryParameter("source", source.name)
.appendQueryParameter("name", title)
.appendQueryParameter("url", url)
.build()

View File

@@ -10,7 +10,7 @@ fun MangaSource.getLocaleTitle(): String? {
} }
fun MangaSource(name: String): MangaSource { fun MangaSource(name: String): MangaSource {
MangaSource.values().forEach { MangaSource.entries.forEach {
if (it.name == name) return it if (it.name == name) return it
} }
return MangaSource.DUMMY return MangaSource.DUMMY

View File

@@ -3,20 +3,21 @@ package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.jsoup.Jsoup
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import java.net.HttpURLConnection.HTTP_FORBIDDEN import java.net.HttpURLConnection.HTTP_FORBIDDEN
import java.net.HttpURLConnection.HTTP_UNAVAILABLE import java.net.HttpURLConnection.HTTP_UNAVAILABLE
private const val HEADER_SERVER = "Server"
private const val SERVER_CLOUDFLARE = "cloudflare"
class CloudFlareInterceptor : Interceptor { class CloudFlareInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request()) val response = chain.proceed(chain.request())
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) { if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) { val content = response.body?.source()?.peek()?.use {
Jsoup.parse(it.inputStream(), Charsets.UTF_8.name(), response.request.url.toString())
} ?: return response
if (content.getElementById("challenge-error-title") != null) {
val request = response.request val request = response.request
response.closeQuietly() response.closeQuietly()
throw CloudFlareProtectedException( throw CloudFlareProtectedException(

View File

@@ -15,6 +15,7 @@ object CommonHeaders {
const val AUTHORIZATION = "Authorization" const val AUTHORIZATION = "Authorization"
const val CACHE_CONTROL = "Cache-Control" const val CACHE_CONTROL = "Cache-Control"
const val PROXY_AUTHORIZATION = "Proxy-Authorization" const val PROXY_AUTHORIZATION = "Proxy-Authorization"
const val RETRY_AFTER = "Retry-After"
val CACHE_CONTROL_NO_STORE: CacheControl val CACHE_CONTROL_NO_STORE: CacheControl
get() = CacheControl.Builder().noStore().build() get() = CacheControl.Builder().noStore().build()

View File

@@ -67,6 +67,7 @@ interface NetworkModule {
cache(cache) cache(cache)
addInterceptor(GZipInterceptor()) addInterceptor(GZipInterceptor())
addInterceptor(CloudFlareInterceptor()) addInterceptor(CloudFlareInterceptor())
addInterceptor(RateLimitInterceptor())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor()) addInterceptor(CurlLoggingInterceptor())
} }

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
class RateLimitInterceptor : Interceptor {
private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZ", Locale.ENGLISH)
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == 429) {
val retryDate = response.header(CommonHeaders.RETRY_AFTER)?.parseRetryDate()
val request = response.request
response.closeQuietly()
throw TooManyRequestExceptions(
url = request.url.toString(),
retryAt = retryDate,
)
}
return response
}
private fun String.parseRetryDate(): Date? {
toIntOrNull()?.let {
return Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(it.toLong()))
}
return dateFormat.parse(this)
}
}

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.core.os
import android.content.Intent
import android.os.Build
import android.provider.Settings
@Suppress("FunctionName")
fun NetworkManageIntent(): Intent {
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Settings.Panel.ACTION_INTERNET_CONNECTIVITY
} else {
Settings.ACTION_WIRELESS_SETTINGS
}
return Intent(action)
}

View File

@@ -17,10 +17,12 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@Reusable @Reusable
class MangaDataRepository @Inject constructor( class MangaDataRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
private val resolverProvider: Provider<MangaLinkResolver>,
) { ) {
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) { suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
@@ -63,10 +65,15 @@ class MangaDataRepository @Inject constructor(
return db.mangaDao.find(mangaId)?.toManga() return db.mangaDao.find(mangaId)?.toManga()
} }
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
return db.mangaDao.findByPublicUrl(publicUrl)?.toManga()
}
suspend fun resolveIntent(intent: MangaIntent): Manga? = when { suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga intent.manga != null -> intent.manga
intent.mangaId != 0L -> findMangaById(intent.mangaId) intent.mangaId != 0L -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri intent.uri != null -> resolverProvider.get().resolve(intent.uri)
else -> null
} }
suspend fun storeManga(manga: Manga) { suspend fun storeManga(manga: Manga) {

View File

@@ -0,0 +1,119 @@
package org.koitharu.kotatsu.core.parser
import android.net.Uri
import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toRelativeUrl
import javax.inject.Inject
@Reusable
class MangaLinkResolver @Inject constructor(
private val repositoryFactory: MangaRepository.Factory,
private val sourcesRepository: MangaSourcesRepository,
private val dataRepository: MangaDataRepository,
) {
suspend fun resolve(uri: Uri): Manga {
return if (uri.host == "kotatsu.app") {
resolveAppLink(uri)
} else {
resolveExternalLink(uri)
} ?: throw NotFoundException("Manga not found", uri.toString())
}
suspend fun resolveAppLink(uri: Uri): Manga? {
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
val source = MangaSource(sourceName)
require(source != MangaSource.DUMMY) { "Manga source $sourceName is not supported" }
val repo = repositoryFactory.create(source)
return repo.findExact(
url = uri.getQueryParameter("url"),
title = uri.getQueryParameter("name"),
)
}
suspend fun resolveExternalLink(uri: Uri): Manga? {
dataRepository.findMangaByPublicUrl(uri.toString())?.let {
return it
}
val host = uri.host ?: return null
val repo = sourcesRepository.allMangaSources.asSequence()
.map { source ->
repositoryFactory.create(source) as RemoteMangaRepository
}.find { repo ->
host in repo.domains
} ?: return null
return repo.findExact(uri.toString().toRelativeUrl(host), null)
}
private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? {
if (!title.isNullOrEmpty()) {
val list = getList(0, title)
if (url != null) {
list.find { it.url == url }?.let {
return it
}
}
list.minByOrNull { it.title.levenshteinDistance(title) }
?.takeIf { it.title.almostEquals(title, 0.2f) }
?.let { return it }
}
val seed = getDetailsNoCache(
getSeedManga(source, url ?: return null, title),
)
return runCatchingCancellable {
val seedTitle = seed.title.ifEmpty {
seed.altTitle
}.ifNullOrEmpty {
seed.author
} ?: return@runCatchingCancellable null
val seedList = getList(0, seedTitle)
seedList.first { x -> x.url == url }
}.getOrThrow()
}
private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga {
return if (this is RemoteMangaRepository) {
getDetails(manga, withCache = false)
} else {
getDetails(manga)
}
}
private fun getSeedManga(source: MangaSource, url: String, title: String?) = Manga(
id = run {
var h = 1125899906842597L
source.name.forEach { c ->
h = 31 * h + c.code
}
url.forEach { c ->
h = 31 * h + c.code
}
h
},
title = title.orEmpty(),
altTitle = null,
url = url,
publicUrl = "",
rating = 0.0f,
isNsfw = source.contentType == ContentType.HENTAI,
coverUrl = "",
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
chapters = null,
source = source,
)
}

View File

@@ -51,6 +51,9 @@ class RemoteMangaRepository(
getConfig()[parser.configKeyDomain] = value getConfig()[parser.configKeyDomain] = value
} }
val domains: Array<out String>
get() = parser.configKeyDomain.presetValues
val headers: Headers val headers: Headers
get() = parser.headers get() = parser.headers
@@ -70,14 +73,7 @@ class RemoteMangaRepository(
return parser.getList(offset, tags, sortOrder) return parser.getList(offset, tags, sortOrder)
} }
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, withCache = true)
cache.getDetails(source, manga.url)?.let { return it }
val details = asyncSafe {
parser.getDetails(manga)
}
cache.putDetails(source, manga.url, details)
return details.await()
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
cache.getPages(source, chapter.url)?.let { return it } cache.getPages(source, chapter.url)?.let { return it }
@@ -103,6 +99,23 @@ class RemoteMangaRepository(
return related.await() return related.await()
} }
suspend fun getDetails(manga: Manga, withCache: Boolean): Manga {
if (!withCache) {
return parser.getDetails(manga)
}
cache.getDetails(source, manga.url)?.let { return it }
val details = asyncSafe {
parser.getDetails(manga)
}
cache.putDetails(source, manga.url, details)
return details.await()
}
suspend fun find(manga: Manga): Manga? {
val list = getList(0, manga.title)
return list.find { x -> x.id == manga.id }
}
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
fun getConfigKeys(): List<ConfigKey<*>> = ArrayList<ConfigKey<*>>().also { fun getConfigKeys(): List<ConfigKey<*>> = ArrayList<ConfigKey<*>>().also {

View File

@@ -21,6 +21,7 @@ import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okio.Closeable import okio.Closeable
import okio.buffer import okio.buffer
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
@@ -49,22 +50,32 @@ class FaviconFetcher(
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
getCached(options)?.let { return it } getCached(options)?.let { return it }
val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
val favicons = repo.getFavicons()
val sizePx = maxOf( val sizePx = maxOf(
options.size.width.pxOrElse { FALLBACK_SIZE }, options.size.width.pxOrElse { FALLBACK_SIZE },
options.size.height.pxOrElse { FALLBACK_SIZE }, options.size.height.pxOrElse { FALLBACK_SIZE },
) )
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" } var favicons = repo.getFavicons()
val response = loadIcon(icon.url, mangaSource) while (favicons.isNotEmpty()) {
val responseBody = response.requireBody() val icon = favicons.find(sizePx) ?: throwNSEE()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also { val response = try {
response.closeQuietly() loadIcon(icon.url, mangaSource)
} ?: responseBody.toImageSource(response) } catch (e: CloudFlareProtectedException) {
return SourceResult( throw e
source = source, } catch (e: HttpException) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type), favicons -= icon
dataSource = response.toDataSource(), continue
) }
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult(
source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
dataSource = response.toDataSource(),
)
}
throwNSEE()
} }
private suspend fun loadIcon(url: String, source: MangaSource): Response { private suspend fun loadIcon(url: String, source: MangaSource): Response {
@@ -143,6 +154,8 @@ class FaviconFetcher(
append(height.toString()) append(height.toString())
} }
private fun throwNSEE(): Nothing = throw NoSuchElementException("No favicons found")
class Factory( class Factory(
context: Context, context: Context,
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,

View File

@@ -2,14 +2,14 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.Settings import android.provider.Settings
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.ConnectivityManagerCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@@ -55,6 +55,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_GRID_SIZE, 100) get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
var isNsfwContentDisabled: Boolean
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }
var appLocales: LocaleListCompat var appLocales: LocaleListCompat
get() { get() {
val raw = prefs.getString(KEY_APP_LOCALE, null) val raw = prefs.getString(KEY_APP_LOCALE, null)
@@ -100,8 +104,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val notificationLight: Boolean val notificationLight: Boolean
get() = prefs.getBoolean(KEY_NOTIFICATIONS_LIGHT, true) get() = prefs.getBoolean(KEY_NOTIFICATIONS_LIGHT, true)
val readerAnimation: Boolean val readerAnimation: ReaderAnimation
get() = prefs.getBoolean(KEY_READER_ANIMATION, false) get() = prefs.getEnumValue(KEY_READER_ANIMATION, ReaderAnimation.DEFAULT)
val readerBackground: ReaderBackground val readerBackground: ReaderBackground
get() = prefs.getEnumValue(KEY_READER_BACKGROUND, ReaderBackground.DEFAULT) get() = prefs.getEnumValue(KEY_READER_BACKGROUND, ReaderBackground.DEFAULT)
@@ -134,7 +138,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER) get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER)
val trackSources: Set<String> val trackSources: Set<String>
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY) get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: setOf(TRACK_FAVOURITES)
var appPassword: String? var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null) get() = prefs.getString(KEY_APP_PASSWORD, null)
@@ -276,6 +280,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, HistoryOrder.UPDATED) get() = prefs.getEnumValue(KEY_HISTORY_ORDER, HistoryOrder.UPDATED)
set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) } set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) }
val isRelatedMangaEnabled: Boolean
get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
val isWebtoonZoomEnable: Boolean val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true) get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
@@ -333,8 +340,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
} }
private fun isBackgroundNetworkRestricted(): Boolean { private fun isBackgroundNetworkRestricted(): Boolean {
return ConnectivityManagerCompat.getRestrictBackgroundStatus(connectivityManager) == return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_ENABLED connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
} else {
false
}
} }
private fun JSONArray.toStringSet(): Set<String> { private fun JSONArray.toStringSet(): Set<String> {
@@ -380,7 +390,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate" const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light" const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info" const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
const val KEY_READER_ANIMATION = "reader_animation" const val KEY_READER_ANIMATION = "reader_animation2"
const val KEY_READER_MODE = "reader_mode" const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect" const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password" const val KEY_APP_PASSWORD = "app_password"
@@ -440,6 +450,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_PASSWORD = "proxy_password" const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy" const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs" const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -4,6 +4,7 @@ import androidx.annotation.StringRes
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.find
enum class ColorScheme( enum class ColorScheme(
@StyleRes val styleResId: Int, @StyleRes val styleResId: Int,
@@ -31,7 +32,7 @@ enum class ColorScheme(
} }
fun getAvailableList(): List<ColorScheme> { fun getAvailableList(): List<ColorScheme> {
val list = enumValues<ColorScheme>().toMutableList() val list = ColorScheme.entries.toMutableList()
if (!DynamicColors.isDynamicColorAvailable()) { if (!DynamicColors.isDynamicColorAvailable()) {
list.remove(MONET) list.remove(MONET)
} }
@@ -39,7 +40,7 @@ enum class ColorScheme(
} }
fun safeValueOf(name: String): ColorScheme? { fun safeValueOf(name: String): ColorScheme? {
return enumValues<ColorScheme>().find { it.name == name } return ColorScheme.entries.find(name)
} }
} }
} }

View File

@@ -20,7 +20,7 @@ enum class NetworkPolicy(
fun from(key: String?, default: NetworkPolicy): NetworkPolicy { fun from(key: String?, default: NetworkPolicy): NetworkPolicy {
val intKey = key?.toIntOrNull() ?: return default val intKey = key?.toIntOrNull() ?: return default
return enumValues<NetworkPolicy>().find { it.key == intKey } ?: default return NetworkPolicy.entries.find { it.key == intKey } ?: default
} }
} }
} }

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.core.prefs
enum class ReaderAnimation {
// Do not rename this
NONE, DEFAULT, ADVANCED;
}

View File

@@ -8,6 +8,6 @@ enum class ReaderMode(val id: Int) {
companion object { companion object {
fun valueOf(id: Int) = values().firstOrNull { it.id == id } fun valueOf(id: Int) = entries.firstOrNull { it.id == id }
} }
} }

View File

@@ -97,7 +97,6 @@ abstract class BaseActivity<B : ViewBinding> :
} }
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) { override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
@Suppress("DEPRECATION")
onBackPressed() onBackPressed()
true true
} else super.onOptionsItemSelected(item) } else super.onOptionsItemSelected(item)

View File

@@ -13,13 +13,10 @@ import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
open class BaseListAdapter<T : ListModel>( open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
vararg delegates: AdapterDelegate<List<T>>,
) : AsyncListDifferDelegationAdapter<T>(
AsyncDifferConfig.Builder(ListModelDiffCallback<T>()) AsyncDifferConfig.Builder(ListModelDiffCallback<T>())
.setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor()) .setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor())
.build(), .build(),
*delegates,
), FlowCollector<List<T>?> { ), FlowCollector<List<T>?> {
override suspend fun emit(value: List<T>?) = suspendCoroutine { cont -> override suspend fun emit(value: List<T>?) = suspendCoroutine { cont ->

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import javax.inject.Inject import javax.inject.Inject
@@ -33,7 +34,8 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(view.context.getThemeColor(android.R.attr.colorBackground)) val themedContext = (view.parentView ?: view).context
view.setBackgroundColor(themedContext.getThemeColor(android.R.attr.colorBackground))
listView.clipToPadding = false listView.clipToPadding = false
insetsDelegate.onViewCreated(view) insetsDelegate.onViewCreated(view)
insetsDelegate.addInsetsListener(this) insetsDelegate.addInsetsListener(this)

View File

@@ -7,10 +7,14 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -32,9 +36,8 @@ abstract class BaseViewModel : ViewModel() {
val onError: EventFlow<Throwable> val onError: EventFlow<Throwable>
get() = errorEvent get() = errorEvent
val isLoading: StateFlow<Boolean> val isLoading: StateFlow<Boolean> = loadingCounter.map { it > 0 }
get() = loadingCounter.map { it > 0 } .stateIn(viewModelScope, SharingStarted.Lazily, loadingCounter.value > 0)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), loadingCounter.value > 0)
protected fun launchJob( protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
@@ -55,14 +58,24 @@ abstract class BaseViewModel : ViewModel() {
} }
} }
protected fun <T> Flow<T>.withLoading() = onStart {
loadingCounter.increment()
}.onCompletion {
loadingCounter.decrement()
}
protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
errorEvent.call(error)
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug() throwable.printStackTraceDebug()
if (throwable !is CancellationException) { if (throwable !is CancellationException) {
errorEvent.call(throwable) errorEvent.call(throwable)
} }
} }
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
} }

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.core.ui.dialog
import android.content.Context
import android.graphics.Color
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetDialog
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
/**
* https://github.com/material-components/material-components-android/issues/2582
*/
@Suppress("DEPRECATION")
override fun onAttachedToWindow() {
val window = window
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
super.onAttachedToWindow()
if (window != null) {
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
if (drawEdgeToEdge) {
// Copied from super.onAttachedToWindow:
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Fix super-class's window flag bug by respecting the initial system UI visibility:
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
}
}
}
}

View File

@@ -1,87 +0,0 @@
package org.koitharu.kotatsu.core.ui.list.decor
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.core.content.res.getColorOrThrow
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.R as materialR
@SuppressLint("PrivateResource")
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val thickness: Int
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
paint.style = Paint.Style.FILL
val ta = context.obtainStyledAttributes(
null,
materialR.styleable.MaterialDivider,
materialR.attr.materialDividerStyle,
materialR.style.Widget_Material3_MaterialDivider,
)
paint.color = ta.getColorOrThrow(materialR.styleable.MaterialDivider_dividerColor)
thickness = ta.getDimensionPixelSize(
materialR.styleable.MaterialDivider_dividerThickness,
context.resources.getDimensionPixelSize(materialR.dimen.material_divider_thickness),
)
ta.recycle()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect.set(0, thickness, 0, 0)
}
// TODO implement for horizontal lists on demand
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
if (parent.layoutManager == null || thickness == 0) {
return
}
canvas.save()
val left: Float
val right: Float
if (parent.clipToPadding) {
left = parent.paddingLeft.toFloat()
right = (parent.width - parent.paddingRight).toFloat()
canvas.clipRect(
left,
parent.paddingTop.toFloat(),
right,
(parent.height - parent.paddingBottom).toFloat(),
)
} else {
left = 0f
right = parent.width.toFloat()
}
var previous: RecyclerView.ViewHolder? = null
for (child in parent.children) {
val holder = parent.getChildViewHolder(child)
if (previous != null && shouldDrawDivider(previous, holder)) {
parent.getDecoratedBoundsWithMargins(child, bounds)
val top: Float = bounds.top + child.translationY
val bottom: Float = top + thickness
canvas.drawRect(left, top, right, bottom, paint)
}
previous = holder
}
canvas.restore()
}
protected abstract fun shouldDrawDivider(
above: RecyclerView.ViewHolder,
below: RecyclerView.ViewHolder,
): Boolean
}

View File

@@ -519,7 +519,7 @@ class FastScroller @JvmOverloads constructor(
private fun TypedArray.getBubbleSize(@StyleableRes index: Int, defaultValue: BubbleSize): BubbleSize { private fun TypedArray.getBubbleSize(@StyleableRes index: Int, defaultValue: BubbleSize): BubbleSize {
val ordinal = getInt(index, -1) val ordinal = getInt(index, -1)
return BubbleSize.values().getOrNull(ordinal) ?: defaultValue return BubbleSize.entries.getOrNull(ordinal) ?: defaultValue
} }
private fun findValidParent(view: View): ViewGroup? = view.parents.firstNotNullOfOrNull { p -> private fun findValidParent(view: View): ViewGroup? = view.parents.firstNotNullOfOrNull { p ->

View File

@@ -8,6 +8,7 @@ import androidx.core.content.FileProvider
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.logs.FileLogger import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.model.appUrl
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.io.File import java.io.File
@@ -22,6 +23,8 @@ class ShareHelper(private val context: Context) {
append(manga.title) append(manga.title)
append("\n \n") append("\n \n")
append(manga.publicUrl) append(manga.publicUrl)
append("\n \n")
append(manga.appUrl)
} }
ShareCompat.IntentBuilder(context) ShareCompat.IntentBuilder(context)
.setText(text) .setText(text)

View File

@@ -1,9 +1,12 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.app.ActivityManager.MemoryInfo import android.app.ActivityManager.MemoryInfo
import android.app.ActivityOptions import android.app.ActivityOptions
import android.app.LocaleConfig
import android.content.Context import android.content.Context
import android.content.Context.ACTIVITY_SERVICE import android.content.Context.ACTIVITY_SERVICE
import android.content.Context.POWER_SERVICE import android.content.Context.POWER_SERVICE
@@ -11,8 +14,8 @@ import android.content.ContextWrapper
import android.content.OperationApplicationException import android.content.OperationApplicationException
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SyncResult import android.content.SyncResult
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.content.res.Resources
import android.database.SQLException import android.database.SQLException
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
@@ -27,6 +30,8 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes import androidx.annotation.IntegerRes
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
@@ -78,7 +83,7 @@ fun <I> ActivityResultLauncher<I>.tryLaunch(
e.printStackTraceDebug() e.printStackTraceDebug()
}.isSuccess }.isSuccess
fun SharedPreferences.observe() = callbackFlow<String> { fun SharedPreferences.observe() = callbackFlow<String?> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
trySendBlocking(key) trySendBlocking(key)
} }
@@ -172,10 +177,17 @@ fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimation
null null
} }
fun Resources.getLocalesConfig(): LocaleListCompat { @SuppressLint("DiscouragedApi")
fun Context.getLocalesConfig(): LocaleListCompat {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
LocaleConfig(this).supportedLocales?.let {
return LocaleListCompat.wrap(it)
}
}
val tagsList = StringJoiner(",") val tagsList = StringJoiner(",")
try { try {
val xpp: XmlPullParser = getXml(R.xml.locales) val resId = resources.getIdentifier("_generated_res_locale_config", "xml", packageName)
val xpp: XmlPullParser = resources.getXml(resId)
while (xpp.eventType != XmlPullParser.END_DOCUMENT) { while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
if (xpp.eventType == XmlPullParser.START_TAG) { if (xpp.eventType == XmlPullParser.START_TAG) {
if (xpp.name == "locale") { if (xpp.name == "locale") {
@@ -212,3 +224,9 @@ inline fun Activity.catchingWebViewUnavailability(block: () -> Unit): Boolean {
} }
} }
} }
fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(this).areNotificationsEnabled()
}

View File

@@ -29,7 +29,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
.data(data) .data(data)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.crossfade(context) .crossfade(context)
.listener(CaptchaNotifier(context.applicationContext)) .addListener(CaptchaNotifier(context.applicationContext))
.target(this) .target(this)
} }
@@ -65,11 +65,11 @@ fun ImageResult.toBitmapOrNull() = when (this) {
} }
fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder { fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder {
return listener(ImageRequestIndicatorListener(listOf(indicator))) return addListener(ImageRequestIndicatorListener(listOf(indicator)))
} }
fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>): ImageRequest.Builder { fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>): ImageRequest.Builder {
return listener(ImageRequestIndicatorListener(indicators)) return addListener(ImageRequestIndicatorListener(indicators))
} }
fun ImageRequest.Builder.decodeRegion( fun ImageRequest.Builder.decodeRegion(
@@ -86,3 +86,30 @@ fun ImageRequest.Builder.crossfade(context: Context): ImageRequest.Builder {
fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder { fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder {
return tag(MangaSource::class.java, source) return tag(MangaSource::class.java, source)
} }
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
val existing = build().listener
return listener(
when (existing) {
null -> listener
is CompositeImageRequestListener -> existing + listener
else -> CompositeImageRequestListener(arrayOf(existing, listener))
},
)
}
private class CompositeImageRequestListener(
private val delegates: Array<ImageRequest.Listener>,
) : ImageRequest.Listener {
override fun onCancel(request: ImageRequest) = delegates.forEach { it.onCancel(request) }
override fun onError(request: ImageRequest, result: ErrorResult) = delegates.forEach { it.onError(request, result) }
override fun onStart(request: ImageRequest) = delegates.forEach { it.onStart(request) }
override fun onSuccess(request: ImageRequest, result: SuccessResult) =
delegates.forEach { it.onSuccess(request, result) }
operator fun plus(other: ImageRequest.Listener) = CompositeImageRequestListener(delegates + other)
}

View File

@@ -5,15 +5,6 @@ import androidx.collection.ArraySet
import java.util.Collections import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
@Deprecated("TODO: remove")
fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
if (sourceIndex <= targetIndex) {
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
} else {
Collections.rotate(subList(targetIndex, sourceIndex + 1), 1)
}
}
inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> { inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> {
val set = ArraySet<T>(size) val set = ArraySet<T>(size)
repeat(size) { index -> set.add(init(index)) } repeat(size) { index -> set.add(init(index)) }
@@ -41,10 +32,6 @@ fun <K, V> Map<K, V>.findKeyByValue(value: V): K? {
return null return null
} }
inline fun <T> Collection<T>.filterToSet(predicate: (T) -> Boolean): Set<T> {
return filterTo(ArraySet(size), predicate)
}
fun <T> Sequence<T>.toListSorted(comparator: Comparator<T>): List<T> { fun <T> Sequence<T>.toListSorted(comparator: Comparator<T>): List<T> {
return toMutableList().apply { sortWith(comparator) } return toMutableList().apply { sortWith(comparator) }
} }

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.fs.FileSequence
import java.io.File import java.io.File
import java.io.FileFilter import java.io.FileFilter
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@@ -73,11 +74,10 @@ suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
@WorkerThread @WorkerThread
private fun computeSizeInternal(file: File): Long { private fun computeSizeInternal(file: File): Long {
if (file.isDirectory) { return if (file.isDirectory) {
val files = file.listFiles() ?: return 0L file.children().sumOf { computeSizeInternal(it) }
return files.sumOf { computeSizeInternal(it) }
} else { } else {
return file.length() file.length()
} }
} }
@@ -86,9 +86,8 @@ fun File.listFilesRecursive(filter: FileFilter? = null): Sequence<File> = sequen
} }
private suspend fun SequenceScope<File>.listFilesRecursiveImpl(root: File, filter: FileFilter?) { private suspend fun SequenceScope<File>.listFilesRecursiveImpl(root: File, filter: FileFilter?) {
val ss = root.list() ?: return val ss = root.children()
for (s in ss) { for (f in ss) {
val f = File(root, s)
if (f.isDirectory) { if (f.isDirectory) {
listFilesRecursiveImpl(f, filter) listFilesRecursiveImpl(f, filter)
} else if (filter == null || filter.accept(f)) { } else if (filter == null || filter.accept(f)) {
@@ -96,3 +95,7 @@ private suspend fun SequenceScope<File>.listFilesRecursiveImpl(root: File, filte
} }
} }
} }
fun File.children() = FileSequence(this)
fun Sequence<File>.filterWith(filter: FileFilter): Sequence<File> = filter { f -> filter.accept(f) }

View File

@@ -4,7 +4,7 @@ import androidx.annotation.FloatRange
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import java.util.UUID import java.util.UUID
inline fun <C : CharSequence> C?.ifNullOrEmpty(defaultValue: () -> C): C { inline fun <C : CharSequence?> C?.ifNullOrEmpty(defaultValue: () -> C): C {
return if (this.isNullOrEmpty()) defaultValue() else this return if (this.isNullOrEmpty()) defaultValue() else this
} }

View File

@@ -9,8 +9,6 @@ import androidx.annotation.ColorInt
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.content.res.TypedArrayUtils
import androidx.core.content.res.use import androidx.core.content.res.use
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils

View File

@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.res.Resources import android.content.res.Resources
import android.util.AndroidRuntimeException import android.util.AndroidRuntimeException
import androidx.annotation.DrawableRes
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import coil.network.HttpException
import okio.FileNotFoundException import okio.FileNotFoundException
import okio.IOException import okio.IOException
import org.acra.ktx.sendWithAcra import org.acra.ktx.sendWithAcra
@@ -14,6 +16,7 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.SyncApiException import org.koitharu.kotatsu.core.exceptions.SyncApiException
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
@@ -32,6 +35,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is UnsupportedOperationException, is UnsupportedOperationException,
-> resources.getString(R.string.operation_not_supported) -> resources.getString(R.string.operation_not_supported)
is TooManyRequestExceptions -> resources.getString(R.string.too_many_requests_message)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is FileNotFoundException -> resources.getString(R.string.file_not_found) is FileNotFoundException -> resources.getString(R.string.file_not_found)
is AccessDeniedException -> resources.getString(R.string.no_access_to_file) is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
@@ -48,10 +52,8 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is WrongPasswordException -> resources.getString(R.string.wrong_password) is WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404) is NotFoundException -> resources.getString(R.string.not_found_404)
is HttpStatusException -> when (statusCode) { is HttpException -> getHttpDisplayMessage(response.code, resources)
in 500..599 -> resources.getString(R.string.server_error, statusCode) is HttpStatusException -> getHttpDisplayMessage(statusCode, resources)
else -> localizedMessage
}
is IOException -> getDisplayMessage(message, resources) ?: localizedMessage is IOException -> getDisplayMessage(message, resources) ?: localizedMessage
else -> localizedMessage else -> localizedMessage
@@ -59,6 +61,23 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
resources.getString(R.string.error_occurred) resources.getString(R.string.error_occurred)
} }
@DrawableRes
fun Throwable.getDisplayIcon() = when (this) {
is AuthRequiredException -> R.drawable.ic_auth_key_large
is CloudFlareProtectedException -> R.drawable.ic_bot_large
is UnknownHostException,
is SocketTimeoutException,
-> R.drawable.ic_plug_large
else -> R.drawable.ic_error_large
}
private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String? = when (statusCode) {
404 -> resources.getString(R.string.not_found_404)
in 500..599 -> resources.getString(R.string.server_error, statusCode)
else -> null
}
private fun getDisplayMessage(msg: String?, resources: Resources): String? = when { private fun getDisplayMessage(msg: String?, resources: Resources): String? = when {
msg.isNullOrEmpty() -> null msg.isNullOrEmpty() -> null
msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left) msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left)

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.view.ViewParent import android.view.ViewParent
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Checkable import android.widget.Checkable
import android.widget.CompoundButton
import androidx.core.view.children import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.core.util.iterator
import okhttp3.internal.closeQuietly
import okio.Closeable
class CloseableIterator<T>(
private val upstream: Iterator<T>,
private val closeable: Closeable,
) : Iterator<T>, Closeable {
private var isClosed = false
override fun hasNext(): Boolean {
val result = upstream.hasNext()
if (!result) {
close()
}
return result
}
override fun next(): T {
try {
return upstream.next()
} catch (e: NoSuchElementException) {
close()
throw e
}
}
override fun close() {
if (!isClosed) {
closeable.closeQuietly()
isClosed = true
}
}
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.util.iterator
import org.koitharu.kotatsu.R
class MappingIterator<T, R>(
private val upstream: Iterator<T>,
private val mapper: (T) -> R,
) : Iterator<R> {
override fun hasNext(): Boolean = upstream.hasNext()
override fun next(): R = mapper(upstream.next())
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.zip
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.collection.ArraySet import androidx.collection.ArraySet
import okio.Closeable import okio.Closeable
import org.koitharu.kotatsu.core.util.ext.children
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.util.zip.Deflater import java.util.zip.Deflater
@@ -90,7 +91,7 @@ class ZipOutput(
} }
putNextEntry(entry) putNextEntry(entry)
closeEntry() closeEntry()
fileToZip.listFiles()?.forEach { childFile -> fileToZip.children().forEach { childFile ->
appendFile(childFile, "$name/${childFile.name}") appendFile(childFile, "$name/${childFile.name}")
} }
} else { } else {

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.core.zip
import android.net.Uri
import androidx.annotation.WorkerThread
import androidx.collection.LruCache
import okhttp3.internal.closeQuietly
import okio.Source
import okio.source
import java.io.File
import java.util.zip.ZipFile
class ZipPool(maxSize: Int) : LruCache<String, ZipFile>(maxSize) {
override fun entryRemoved(evicted: Boolean, key: String, oldValue: ZipFile, newValue: ZipFile?) {
super.entryRemoved(evicted, key, oldValue, newValue)
oldValue.closeQuietly()
}
override fun create(key: String): ZipFile {
return ZipFile(File(key), ZipFile.OPEN_READ)
}
@Synchronized
@WorkerThread
operator fun get(uri: Uri): Source {
val zip = requireNotNull(get(uri.schemeSpecificPart)) {
"Cannot obtain zip by \"$uri\""
}
val entry = zip.getEntry(uri.fragment)
return zip.getInputStream(entry).source()
}
}

View File

@@ -87,7 +87,6 @@ class ChaptersFragment :
.manga(viewModel.manga.value ?: return) .manga(viewModel.manga.value ?: return)
.state(ReaderState(item.chapter.id, 0, 0)) .state(ReaderState(item.chapter.id, 0, 0))
.build(), .build(),
scaleUpActivityOptionsOf(view),
) )
} }

View File

@@ -1,13 +1,16 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.transition.AutoTransition import android.transition.AutoTransition
import android.transition.Slide import android.transition.Slide
import android.transition.TransitionManager import android.transition.TransitionManager
import android.view.Gravity import android.view.Gravity
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -17,6 +20,8 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@@ -33,12 +38,11 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ViewBadge import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.measureHeight import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
@@ -48,10 +52,8 @@ import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.adapter.branchAD
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -157,7 +159,7 @@ class DetailsActivity :
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false) R.id.button_read -> openReader(isIncognitoMode = false)
R.id.button_dropdown -> showBranchPopupMenu() R.id.button_dropdown -> showBranchPopupMenu(v)
} }
} }
@@ -275,20 +277,28 @@ class DetailsActivity :
viewBadge.counter = newChapters viewBadge.counter = newChapters
} }
private fun showBranchPopupMenu() { private fun showBranchPopupMenu(v: View) {
var dialog: DialogInterface? = null val menu = PopupMenu(v.context, v)
val listener = OnListItemClickListener<MangaBranch> { item, _ -> val branches = viewModel.branches.value
viewModel.setSelectedBranch(item.name) for ((i, branch) in branches.withIndex()) {
dialog?.dismiss() val title = buildSpannedString {
append(branch.name ?: getString(R.string.system_default))
append(' ')
append(' ')
inSpans(
ForegroundColorSpan(v.context.getThemeColor(android.R.attr.textColorSecondary, Color.LTGRAY)),
RelativeSizeSpan(0.74f),
) {
append(branch.count.toString())
}
}
menu.menu.add(Menu.NONE, Menu.NONE, i, title)
} }
dialog = RecyclerViewAlertDialog.Builder<MangaBranch>(this) menu.setOnMenuItemClickListener {
.addAdapterDelegate(branchAD(listener)) viewModel.setSelectedBranch(branches.getOrNull(it.order)?.name)
.setCancelable(true) true
.setNegativeButton(android.R.string.cancel, null) }
.setTitle(R.string.translations) menu.show()
.setItems(viewModel.branches.value)
.create()
.also { it.show() }
} }
private fun openReader(isIncognitoMode: Boolean) { private fun openReader(isIncognitoMode: Boolean) {

View File

@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel
@@ -119,7 +120,6 @@ class DetailsFragment :
override fun onItemClick(item: Bookmark, view: View) { override fun onItemClick(item: Bookmark, view: View) {
startActivity( startActivity(
ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(), ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
scaleUpActivityOptionsOf(view),
) )
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
} }
@@ -228,14 +228,16 @@ class DetailsFragment :
val rv = viewBinding?.recyclerViewRelated ?: return val rv = viewBinding?.recyclerViewRelated ?: return
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter( val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter<ListModel>()
mangaGridItemAD( .addDelegate(
coil, viewLifecycleOwner, ListItemType.MANGA_GRID,
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)), mangaGridItemAD(
) { item, view -> coil, viewLifecycleOwner,
startActivity(DetailsActivity.newIntent(view.context, item), scaleUpActivityOptionsOf(view)) StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
}, ) { item, view ->
).also { rv.adapter = it } startActivity(DetailsActivity.newIntent(view.context, item))
},
).also { rv.adapter = it }
adapter.items = related adapter.items = related
requireViewBinding().groupRelated.isVisible = true requireViewBinding().groupRelated.isVisible = true
} }

View File

@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
@@ -62,7 +62,7 @@ class DetailsMenuProvider(
R.id.action_favourite -> { R.id.action_favourite -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
FavouriteCategoriesSheet.show(activity.supportFragmentManager, it) FavouriteSheet.show(activity.supportFragmentManager, it)
} }
} }

View File

@@ -167,7 +167,7 @@ class DetailsViewModel @Inject constructor(
it?.remote it?.remote
}.distinctUntilChangedBy { it?.id } }.distinctUntilChangedBy { it?.id }
.mapLatest { .mapLatest {
if (it != null) { if (it != null && settings.isRelatedMangaEnabled) {
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty() relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
} else { } else {
emptyList() emptyList()

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.details.ui.adapter
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.details.ui.model.MangaBranch
class BranchesAdapter(
list: List<MangaBranch>,
listener: OnListItemClickListener<MangaBranch>,
) : ListDelegationAdapter<List<MangaBranch>>() {
init {
delegatesManager.addDelegate(branchAD(listener))
items = list
}
}

View File

@@ -88,7 +88,7 @@ class ScrobblingInfoSheet :
viewModel.updateScrobbling( viewModel.updateScrobbling(
index = scrobblerIndex, index = scrobblerIndex,
rating = requireViewBinding().ratingBar.rating / requireViewBinding().ratingBar.numStars, rating = requireViewBinding().ratingBar.rating / requireViewBinding().ratingBar.numStars,
status = enumValues<ScrobblingStatus>().getOrNull(position), status = ScrobblingStatus.entries.getOrNull(position),
) )
} }
@@ -99,7 +99,7 @@ class ScrobblingInfoSheet :
viewModel.updateScrobbling( viewModel.updateScrobbling(
index = scrobblerIndex, index = scrobblerIndex,
rating = rating / ratingBar.numStars, rating = rating / ratingBar.numStars,
status = enumValues<ScrobblingStatus>().getOrNull(requireViewBinding().spinnerStatus.selectedItemPosition), status = ScrobblingStatus.entries.getOrNull(requireViewBinding().spinnerStatus.selectedItemPosition),
) )
} }
} }

View File

@@ -7,6 +7,7 @@ import androidx.work.WorkInfo
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -53,6 +54,7 @@ fun downloadItemAD(
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
transformations(TrimTransformation())
source(item.manga.source) source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }

View File

@@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.Px
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@@ -37,16 +36,12 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
private val viewModel by viewModels<DownloadsViewModel>() private val viewModel by viewModels<DownloadsViewModel>()
private lateinit var selectionController: ListSelectionController private lateinit var selectionController: ListSelectionController
@Px
private var listSpacing = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater)) setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
listSpacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val downloadsAdapter = DownloadsAdapter(this, coil, this) val downloadsAdapter = DownloadsAdapter(this, coil, this)
val decoration = TypedListSpacingDecoration(this) val decoration = TypedListSpacingDecoration(this, false)
selectionController = ListSelectionController( selectionController = ListSelectionController(
activity = this, activity = this,
decoration = DownloadsSelectionDecoration(this), decoration = DownloadsSelectionDecoration(this),
@@ -71,9 +66,10 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.recyclerView.updatePadding( val rv = viewBinding.recyclerView
left = insets.left + listSpacing, rv.updatePadding(
right = insets.right + listSpacing, left = insets.left + rv.paddingTop,
right = insets.right + rv.paddingTop,
bottom = insets.bottom, bottom = insets.bottom,
) )
viewBinding.toolbar.updatePadding( viewBinding.toolbar.updatePadding(
@@ -98,11 +94,11 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
} }
override fun onPauseClick(item: DownloadItemModel) { override fun onPauseClick(item: DownloadItemModel) {
sendBroadcast(PausingReceiver.getPauseIntent(item.id)) sendBroadcast(PausingReceiver.getPauseIntent(this, item.id))
} }
override fun onResumeClick(item: DownloadItemModel) { override fun onResumeClick(item: DownloadItemModel) {
sendBroadcast(PausingReceiver.getResumeIntent(item.id)) sendBroadcast(PausingReceiver.getResumeIntent(this, item.id))
} }
override fun onSelectionChanged(controller: ListSelectionController, count: Int) { override fun onSelectionChanged(controller: ListSelectionController, count: Int) {

View File

@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.Date import java.util.Date
import java.util.LinkedList
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@@ -181,16 +182,34 @@ class DownloadsViewModel @Inject constructor(
if (isEmpty()) { if (isEmpty()) {
return emptyStateList() return emptyStateList()
} }
val destination = ArrayList<ListModel>((size * 1.4).toInt()) val queued = LinkedList<ListModel>()
val running = LinkedList<ListModel>()
val destination = ArrayDeque<ListModel>((size * 1.4).toInt())
var prevDate: DateTimeAgo? = null var prevDate: DateTimeAgo? = null
for (item in this) { for (item in this) {
val date = timeAgo(item.timestamp) when (item.workState) {
if (prevDate != date) { WorkInfo.State.RUNNING -> running += item
destination += ListHeader(date) WorkInfo.State.BLOCKED,
WorkInfo.State.ENQUEUED -> queued += item
else -> {
val date = timeAgo(item.timestamp)
if (prevDate != date) {
destination += ListHeader(date)
}
prevDate = date
destination += item
}
} }
prevDate = date
destination += item
} }
if (running.isNotEmpty()) {
running.addFirst(ListHeader(R.string.in_progress))
}
destination.addAll(0, running)
if (queued.isNotEmpty()) {
queued.addFirst(ListHeader(R.string.queued))
}
destination.addAll(0, queued)
return destination return destination
} }

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.download.ui.worker
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
@@ -130,10 +132,18 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
override suspend fun getForegroundInfo() = ForegroundInfo( override suspend fun getForegroundInfo() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
id.hashCode(), ForegroundInfo(
notificationFactory.create(lastPublishedState), id.hashCode(),
) notificationFactory.create(lastPublishedState),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
ForegroundInfo(
id.hashCode(),
notificationFactory.create(lastPublishedState),
)
}
private suspend fun downloadMangaImpl( private suspend fun downloadMangaImpl(
includedIds: LongArray?, includedIds: LongArray?,
@@ -389,12 +399,12 @@ class DownloadWorker @AssistedInject constructor(
} }
fun pause(id: UUID) { fun pause(id: UUID) {
val intent = PausingReceiver.getPauseIntent(id) val intent = PausingReceiver.getPauseIntent(context, id)
context.sendBroadcast(intent) context.sendBroadcast(intent)
} }
fun resume(id: UUID) { fun resume(id: UUID) {
val intent = PausingReceiver.getResumeIntent(id) val intent = PausingReceiver.getResumeIntent(context, id)
context.sendBroadcast(intent) context.sendBroadcast(intent)
} }

View File

@@ -40,18 +40,20 @@ class PausingReceiver(
addDataPath(id.toString(), PatternMatcher.PATTERN_SIMPLE_GLOB) addDataPath(id.toString(), PatternMatcher.PATTERN_SIMPLE_GLOB)
} }
fun getPauseIntent(id: UUID) = Intent(ACTION_PAUSE) fun getPauseIntent(context: Context, id: UUID) = Intent(ACTION_PAUSE)
.setData(Uri.parse("$SCHEME://$id")) .setData(Uri.parse("$SCHEME://$id"))
.setPackage(context.packageName)
.putExtra(EXTRA_UUID, id.toString()) .putExtra(EXTRA_UUID, id.toString())
fun getResumeIntent(id: UUID) = Intent(ACTION_RESUME) fun getResumeIntent(context: Context, id: UUID) = Intent(ACTION_RESUME)
.setData(Uri.parse("$SCHEME://$id")) .setData(Uri.parse("$SCHEME://$id"))
.setPackage(context.packageName)
.putExtra(EXTRA_UUID, id.toString()) .putExtra(EXTRA_UUID, id.toString())
fun createPausePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast( fun createPausePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast(
context, context,
0, 0,
getPauseIntent(id), getPauseIntent(context, id),
0, 0,
false, false,
) )
@@ -59,7 +61,7 @@ class PausingReceiver(
fun createResumePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast( fun createResumePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast(
context, context,
0, 0,
getResumeIntent(id), getResumeIntent(context, id),
0, 0,
false, false,
) )

View File

@@ -4,13 +4,17 @@ import androidx.room.withTransaction
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.move import org.koitharu.kotatsu.parsers.util.move
import java.util.Collections import java.util.Collections
@@ -20,6 +24,7 @@ import javax.inject.Inject
@Reusable @Reusable
class MangaSourcesRepository @Inject constructor( class MangaSourcesRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
private val settings: AppSettings,
) { ) {
private val dao: MangaSourcesDao private val dao: MangaSourcesDao
@@ -36,11 +41,13 @@ class MangaSourcesRepository @Inject constructor(
get() = Collections.unmodifiableSet(remoteSources) get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List<MangaSource> { suspend fun getEnabledSources(): List<MangaSource> {
return dao.findAllEnabled().toSources() return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled)
} }
fun observeEnabledSources(): Flow<List<MangaSource>> = dao.observeEnabled().map { fun observeEnabledSources(): Flow<List<MangaSource>> = observeIsNsfwDisabled().flatMapLatest { skipNsfw ->
it.toSources() dao.observeEnabled().map {
it.toSources(skipNsfw)
}
} }
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities -> fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
@@ -61,6 +68,14 @@ class MangaSourcesRepository @Inject constructor(
} }
} }
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
db.withTransaction {
for (s in remoteSources) {
dao.setEnabled(s.name, s in sources)
}
}
}
suspend fun setSourcesEnabled(sources: Iterable<MangaSource>, isEnabled: Boolean) { suspend fun setSourcesEnabled(sources: Iterable<MangaSource>, isEnabled: Boolean) {
db.withTransaction { db.withTransaction {
for (s in sources) { for (s in sources) {
@@ -137,14 +152,21 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAll().isEmpty() return dao.findAll().isEmpty()
} }
private fun List<MangaSourceEntity>.toSources(): List<MangaSource> { private fun List<MangaSourceEntity>.toSources(skipNsfwSources: Boolean): List<MangaSource> {
val result = ArrayList<MangaSource>(size) val result = ArrayList<MangaSource>(size)
for (entity in this) { for (entity in this) {
val source = MangaSource(entity.source) val source = MangaSource(entity.source)
if (skipNsfwSources && source.contentType == ContentType.HENTAI) {
continue
}
if (source in remoteSources) { if (source in remoteSources) {
result.add(source) result.add(source)
} }
} }
return result return result
} }
private fun observeIsNsfwDisabled() = settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) {
isNsfwContentDisabled
}
} }

View File

@@ -28,13 +28,13 @@ import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentExploreBinding import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.TipModel import org.koitharu.kotatsu.list.ui.model.TipModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -57,7 +57,6 @@ class ExploreFragment :
private val viewModel by viewModels<ExploreViewModel>() private val viewModel by viewModels<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null private var exploreAdapter: ExploreAdapter? = null
private var paddingHorizontal = 0
override val recyclerView: RecyclerView override val recyclerView: RecyclerView
get() = requireViewBinding().recyclerView get() = requireViewBinding().recyclerView
@@ -69,14 +68,13 @@ class ExploreFragment :
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this, this) { manga, view -> exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this, this) { manga, view ->
startActivity(DetailsActivity.newIntent(view.context, manga), scaleUpActivityOptionsOf(view)) startActivity(DetailsActivity.newIntent(view.context, manga))
} }
with(binding.recyclerView) { with(binding.recyclerView) {
adapter = exploreAdapter adapter = exploreAdapter
setHasFixedSize(true) setHasFixedSize(true)
SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach() SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach()
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) addItemDecoration(TypedListSpacingDecoration(context, false))
paddingHorizontal = spacing
} }
addMenuProvider(ExploreMenuProvider(binding.root.context, viewModel)) addMenuProvider(ExploreMenuProvider(binding.root.context, viewModel))
viewModel.content.observe(viewLifecycleOwner) { viewModel.content.observe(viewLifecycleOwner) {
@@ -97,8 +95,9 @@ class ExploreFragment :
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
requireViewBinding().recyclerView.updatePadding( val rv = requireViewBinding().recyclerView
bottom = insets.bottom, rv.updatePadding(
bottom = insets.bottom + rv.paddingTop,
) )
} }

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
class RecommendationsItem( data class RecommendationsItem(
val manga: Manga val manga: Manga
) : ListModel { ) : ListModel {
@@ -12,18 +12,4 @@ class RecommendationsItem(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is RecommendationsItem return other is RecommendationsItem
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RecommendationsItem
return manga == other.manga
}
override fun hashCode(): Int {
return 31 * manga.hashCode()
}
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.favourites.domain.model package org.koitharu.kotatsu.favourites.domain.model
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.find
class Cover( class Cover(
val url: String, val url: String,
@@ -8,7 +9,7 @@ class Cover(
) { ) {
val mangaSource: MangaSource? val mangaSource: MangaSource?
get() = if (source.isEmpty()) null else MangaSource.values().find { it.name == source } get() = if (source.isEmpty()) null else MangaSource.entries.find(source)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import javax.inject.Inject import javax.inject.Inject
@@ -58,6 +59,7 @@ class FavouriteCategoriesActivity :
selectionController.attachToRecyclerView(viewBinding.recyclerView) selectionController.attachToRecyclerView(viewBinding.recyclerView)
viewBinding.recyclerView.setHasFixedSize(true) viewBinding.recyclerView.setHasFixedSize(true)
viewBinding.recyclerView.adapter = adapter viewBinding.recyclerView.adapter = adapter
viewBinding.recyclerView.addItemDecoration(TypedListSpacingDecoration(this, false))
viewBinding.fabAdd.setOnClickListener(this) viewBinding.fabAdd.setOnClickListener(this)
reorderHelper = ItemTouchHelper(ReorderHelperCallback()).apply { reorderHelper = ItemTouchHelper(ReorderHelperCallback()).apply {
@@ -106,7 +108,7 @@ class FavouriteCategoriesActivity :
right = insets.right, right = insets.right,
) )
viewBinding.recyclerView.updatePadding( viewBinding.recyclerView.updatePadding(
bottom = insets.bottom, bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
) )
} }

View File

@@ -35,6 +35,7 @@ class FavouritesCategoriesViewModel @Inject constructor(
mangaCount = covers.size, mangaCount = covers.size,
covers = covers.take(3), covers = covers.take(3),
category = category, category = category,
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
) )
}.ifEmpty { }.ifEmpty {
listOf( listOf(

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
@@ -17,8 +18,8 @@ class CategoriesAdapter(
) : BaseListAdapter<ListModel>() { ) : BaseListAdapter<ListModel>() {
init { init {
delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener)) addDelegate(ListItemType.CATEGORY_LARGE ,categoryAD(coil, lifecycleOwner, onItemClickListener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listListener)) addDelegate(ListItemType.STATE_EMPTY ,emptyStateListAD(coil, lifecycleOwner, listListener))
.addDelegate(loadingStateAD()) addDelegate(ListItemType.STATE_LOADING ,loadingStateAD())
} }
} }

View File

@@ -8,6 +8,7 @@ class CategoryListModel(
val mangaCount: Int, val mangaCount: Int,
val covers: List<Cover>, val covers: List<Cover>,
val category: FavouriteCategory, val category: FavouriteCategory,
val isTrackerEnabled: Boolean,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
@@ -21,6 +22,7 @@ class CategoryListModel(
other as CategoryListModel other as CategoryListModel
if (mangaCount != other.mangaCount) return false if (mangaCount != other.mangaCount) return false
if (isTrackerEnabled != other.isTrackerEnabled) return false
if (covers != other.covers) return false if (covers != other.covers) return false
if (category.id != other.category.id) return false if (category.id != other.category.id) return false
if (category.title != other.category.title) return false if (category.title != other.category.title) return false
@@ -33,6 +35,7 @@ class CategoryListModel(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = mangaCount var result = mangaCount
result = 31 * result + isTrackerEnabled.hashCode()
result = 31 * result + covers.hashCode() result = 31 * result + covers.hashCode()
result = 31 * result + category.id.hashCode() result = 31 * result + category.id.hashCode()
result = 31 * result + category.title.hashCode() result = 31 * result + category.title.hashCode()

View File

@@ -23,7 +23,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@AndroidEntryPoint @AndroidEntryPoint
class FavouriteCategoriesSheet : class FavouriteSheet :
BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(),
OnListItemClickListener<MangaCategoryItem> { OnListItemClickListener<MangaCategoryItem> {
@@ -53,7 +53,7 @@ class FavouriteCategoriesSheet :
} }
override fun onItemClick(item: MangaCategoryItem, view: View) { override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.id, !item.isChecked) viewModel.setChecked(item.category.id, !item.isChecked)
} }
private fun onContentChanged(categories: List<ListModel>) { private fun onContentChanged(categories: List<ListModel>) {
@@ -72,7 +72,7 @@ class FavouriteCategoriesSheet :
fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga)) fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga))
fun show(fm: FragmentManager, manga: Collection<Manga>) = fun show(fm: FragmentManager, manga: Collection<Manga>) =
FavouriteCategoriesSheet().withArgs(1) { FavouriteSheet().withArgs(1) {
putParcelableArrayList( putParcelableArrayList(
KEY_MANGA_LIST, KEY_MANGA_LIST,
manga.mapTo(ArrayList(manga.size)) { manga.mapTo(ArrayList(manga.size)) {

View File

@@ -11,10 +11,11 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet.Companion.KEY_MANGA_LIST
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -24,6 +25,7 @@ import javax.inject.Inject
class MangaCategoriesViewModel @Inject constructor( class MangaCategoriesViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,
settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
private val manga = savedStateHandle.require<List<ParcelableManga>>(KEY_MANGA_LIST).map { it.manga } private val manga = savedStateHandle.require<List<ParcelableManga>>(KEY_MANGA_LIST).map { it.manga }
@@ -37,9 +39,9 @@ class MangaCategoriesViewModel @Inject constructor(
add(header) add(header)
all.mapTo(this) { all.mapTo(this) {
MangaCategoryItem( MangaCategoryItem(
id = it.id, category = it,
name = it.title,
isChecked = it.id in checked, isChecked = it.id in checked,
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
) )
} }
} }

View File

@@ -1,17 +1,18 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding import org.koitharu.kotatsu.databinding.ItemCategoryCheckableBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun mangaCategoryAD( fun mangaCategoryAD(
clickListener: OnListItemClickListener<MangaCategoryItem>, clickListener: OnListItemClickListener<MangaCategoryItem>,
) = adapterDelegateViewBinding<MangaCategoryItem, ListModel, ItemCheckableNewBinding>( ) = adapterDelegateViewBinding<MangaCategoryItem, ListModel, ItemCategoryCheckableBinding>(
{ inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemCategoryCheckableBinding.inflate(inflater, parent, false) },
) { ) {
itemView.setOnClickListener { itemView.setOnClickListener {
@@ -19,9 +20,9 @@ fun mangaCategoryAD(
} }
bind { payloads -> bind { payloads ->
with(binding.root) { binding.checkableImageView.setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED !in payloads)
text = item.name binding.textViewTitle.text = item.category.title
setChecked(item.isChecked, ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads) binding.imageViewTracker.isVisible = item.category.isTrackingEnabled && item.isTrackerEnabled
} binding.imageViewVisible.isVisible = item.category.isVisibleInLibrary
} }
} }

View File

@@ -1,16 +1,17 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model package org.koitharu.kotatsu.favourites.ui.categories.select.model
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaCategoryItem( data class MangaCategoryItem(
val id: Long, val category: FavouriteCategory,
val name: String,
val isChecked: Boolean, val isChecked: Boolean,
val isTrackerEnabled: Boolean,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaCategoryItem && other.id == id return other is MangaCategoryItem && other.category.id == category.id
} }
override fun getChangePayload(previousState: ListModel): Any? { override fun getChangePayload(previousState: ListModel): Any? {
@@ -20,22 +21,4 @@ class MangaCategoryItem(
super.getChangePayload(previousState) super.getChangePayload(previousState)
} }
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaCategoryItem
if (id != other.id) return false
if (name != other.name) return false
return isChecked == other.isChecked
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + isChecked.hashCode()
return result
}
} }

View File

@@ -38,7 +38,7 @@ class FavouritesListMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.groupId == R.id.group_order) { if (menuItem.groupId == R.id.group_order) {
val order = enumValues<SortOrder>()[menuItem.order] val order = SortOrder.entries[menuItem.order]
viewModel.setSortOrder(order) viewModel.setSortOrder(order)
return true return true
} }

View File

@@ -33,7 +33,7 @@ class FilterSheetFragment :
val adapter = FilterAdapter(filter, this) val adapter = FilterAdapter(filter, this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
filter.filterItems.observe(viewLifecycleOwner, adapter) filter.filterItems.observe(viewLifecycleOwner, adapter)
binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.root.context)) binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.root.context, false))
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.recyclerView.scrollIndicators = 0 binding.recyclerView.scrollIndicators = 0

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.filter.ui.model package org.koitharu.kotatsu.filter.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
class FilterHeaderModel( class FilterHeaderModel(

View File

@@ -12,5 +12,5 @@ enum class HistoryOrder(
PROGRESS(R.string.progress), PROGRESS(R.string.progress),
ALPHABETIC(R.string.by_name); ALPHABETIC(R.string.by_name);
fun isGroupingSupported() = this == UPDATED || this == CREATED fun isGroupingSupported() = this == UPDATED || this == CREATED || this == PROGRESS
} }

View File

@@ -7,12 +7,14 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
class HistoryListAdapter( class HistoryListAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: MangaListListener listener: MangaListListener,
) : MangaListAdapter(coil, lifecycleOwner, listener), FastScroller.SectionIndexer { sizeResolver: ItemSizeResolver,
) : MangaListAdapter(coil, lifecycleOwner, listener, sizeResolver), FastScroller.SectionIndexer {
override fun getSectionText(context: Context, position: Int): CharSequence? { override fun getSectionText(context: Context, position: Int): CharSequence? {
val list = items val list = items

View File

@@ -7,12 +7,14 @@ import androidx.appcompat.view.ActionMode
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@AndroidEntryPoint @AndroidEntryPoint
@@ -31,6 +33,10 @@ class HistoryListFragment : MangaListFragment() {
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
override fun onEmptyActionClick() {
startActivity(NetworkManageIntent())
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_history, menu) mode.menuInflater.inflate(R.menu.mode_history, menu)
return super.onCreateActionMode(controller, mode, menu) return super.onCreateActionMode(controller, mode, menu)
@@ -55,5 +61,10 @@ class HistoryListFragment : MangaListFragment() {
} }
} }
override fun onCreateAdapter() = HistoryListAdapter(coil, viewLifecycleOwner, this) override fun onCreateAdapter() = HistoryListAdapter(
coil,
viewLifecycleOwner,
this,
DynamicItemSizeResolver(resources, settings, adjustWidth = false),
)
} }

View File

@@ -23,7 +23,7 @@ class HistoryListMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_history, menu) menuInflater.inflate(R.menu.opt_history, menu)
val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return
for (order in HistoryOrder.values()) { for (order in HistoryOrder.entries) {
subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleResId) subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleResId)
} }
subMenu.setGroupCheckable(R.id.group_order, true, true) subMenu.setGroupCheckable(R.id.group_order, true, true)
@@ -31,7 +31,7 @@ class HistoryListMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.groupId == R.id.group_order) { if (menuItem.groupId == R.id.group_order) {
val order = enumValues<HistoryOrder>()[menuItem.order] val order = HistoryOrder.entries[menuItem.order]
viewModel.setSortOrder(order) viewModel.setSortOrder(order)
return true return true
} }
@@ -51,7 +51,7 @@ class HistoryListMenuProvider(
} }
override fun onPrepareMenu(menu: Menu) { override fun onPrepareMenu(menu: Menu) {
val order = viewModel.sortOrder.value ?: return val order = viewModel.sortOrder.value
menu.findItem(R.id.action_order)?.subMenu?.forEach { item -> menu.findItem(R.id.action_order)?.subMenu?.forEach { item ->
if (item.order == order.ordinal) { if (item.order == order.ordinal) {
item.isChecked = true item.isChecked = true

View File

@@ -12,6 +12,9 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -27,6 +30,7 @@ import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -35,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@@ -44,6 +49,8 @@ class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val extraProvider: ListExtraProvider, private val extraProvider: ListExtraProvider,
private val localMangaRepository: LocalMangaRepository,
networkState: NetworkState,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) { ) : MangaListViewModel(settings, downloadScheduler) {
@@ -68,7 +75,8 @@ class HistoryListViewModel @Inject constructor(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) }, sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
isGroupingEnabled, isGroupingEnabled,
listMode, listMode,
) { list, grouped, mode -> networkState,
) { list, grouped, mode, online ->
when { when {
list.isEmpty() -> listOf( list.isEmpty() -> listOf(
EmptyState( EmptyState(
@@ -79,7 +87,7 @@ class HistoryListViewModel @Inject constructor(
), ),
) )
else -> mapList(list, grouped, mode) else -> mapList(list, grouped, mode, online)
} }
}.onStart { }.onStart {
loadingCounter.increment() loadingCounter.increment()
@@ -128,16 +136,33 @@ class HistoryListViewModel @Inject constructor(
list: List<MangaWithHistory>, list: List<MangaWithHistory>,
grouped: Boolean, grouped: Boolean,
mode: ListMode, mode: ListMode,
isOnline: Boolean,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1) val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
var prevDate: DateTimeAgo? = null val order = sortOrder.value
for ((manga, history) in list) { var prevHeader: ListHeader? = null
if (!isOnline) {
result += EmptyHint(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.network_unavailable,
textSecondary = R.string.network_unavailable_hint,
actionStringRes = R.string.manage,
)
}
for ((m, history) in list) {
val manga = if (!isOnline && !m.isLocal) {
localMangaRepository.findSavedManga(m)?.manga ?: continue
} else {
m
}
if (grouped) { if (grouped) {
val date = timeAgo(history.updatedAt) val header = history.header(order)
if (prevDate != date) { if (header != prevHeader) {
result += ListHeader(date) if (header != null) {
result += header
}
prevHeader = header
} }
prevDate = date
} }
result += when (mode) { result += when (mode) {
ListMode.LIST -> manga.toListModel(extraProvider) ListMode.LIST -> manga.toListModel(extraProvider)
@@ -148,6 +173,21 @@ class HistoryListViewModel @Inject constructor(
return result return result
} }
private fun MangaHistory.header(order: HistoryOrder): ListHeader? = when (order) {
HistoryOrder.UPDATED -> ListHeader(timeAgo(updatedAt))
HistoryOrder.CREATED -> ListHeader(timeAgo(createdAt))
HistoryOrder.PROGRESS -> ListHeader(
when (percent) {
1f -> R.string.status_completed
in 0f..0.01f -> R.string.status_planned
in 0f..1f -> R.string.status_reading
else -> R.string.unknown
},
)
HistoryOrder.ALPHABETIC -> null
}
private fun timeAgo(date: Date): DateTimeAgo { private fun timeAgo(date: Date): DateTimeAgo {
val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L) val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L)
val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt() val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt()

View File

@@ -5,32 +5,40 @@ import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil.ImageLoader
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ErrorResult
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.target.ViewTarget import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.indicator
import org.koitharu.kotatsu.databinding.ActivityImageBinding import org.koitharu.kotatsu.databinding.ActivityImageBinding
import org.koitharu.kotatsu.databinding.ItemErrorStateBinding
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ImageActivity : BaseActivity<ActivityImageBinding>() { class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listener, View.OnClickListener {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
private var errorBinding: ItemErrorStateBinding? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityImageBinding.inflate(layoutInflater)) setContentView(ActivityImageBinding.inflate(layoutInflater))
@@ -53,14 +61,40 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
} }
} }
override fun onClick(v: View?) {
loadImage(intent.data)
}
override fun onError(request: ImageRequest, result: ErrorResult) {
viewBinding.progressBar.hide()
with(errorBinding ?: ItemErrorStateBinding.bind(viewBinding.stubError.inflate())) {
errorBinding = this
root.isVisible = true
textViewError.text = result.throwable.getDisplayMessage(resources)
textViewError.setCompoundDrawablesWithIntrinsicBounds(0, result.throwable.getDisplayIcon(), 0, 0)
buttonRetry.isVisible = true
buttonRetry.setOnClickListener(this@ImageActivity)
}
}
override fun onStart(request: ImageRequest) {
viewBinding.progressBar.show()
(errorBinding?.root ?: viewBinding.stubError).isVisible = false
}
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
viewBinding.progressBar.hide()
(errorBinding?.root ?: viewBinding.stubError).isVisible = false
}
private fun loadImage(url: Uri?) { private fun loadImage(url: Uri?) {
ImageRequest.Builder(this) ImageRequest.Builder(this)
.data(url) .data(url)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.lifecycle(this) .lifecycle(this)
.listener(this)
.tag(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE)) .tag(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE))
.target(SsivTarget(viewBinding.ssiv)) .target(SsivTarget(viewBinding.ssiv))
.indicator(viewBinding.progressBar)
.enqueueWith(coil) .enqueueWith(coil)
} }

View File

@@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.FitHeightGridLayoutManager import org.koitharu.kotatsu.core.ui.list.FitHeightGridLayoutManager
@@ -36,12 +37,11 @@ import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -49,6 +49,7 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -69,6 +70,9 @@ abstract class MangaListFragment :
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private var listAdapter: MangaListAdapter? = null private var listAdapter: MangaListAdapter? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
private var selectionController: ListSelectionController? = null private var selectionController: ListSelectionController? = null
@@ -104,7 +108,7 @@ abstract class MangaListFragment :
setHasFixedSize(true) setHasFixedSize(true)
adapter = listAdapter adapter = listAdapter
checkNotNull(selectionController).attachToRecyclerView(binding.recyclerView) checkNotNull(selectionController).attachToRecyclerView(binding.recyclerView)
addItemDecoration(TypedListSpacingDecoration(context)) addItemDecoration(TypedListSpacingDecoration(context, false))
addOnScrollListener(paginationListener!!) addOnScrollListener(paginationListener!!)
fastScroller.setFastScrollListener(this@MangaListFragment) fastScroller.setFastScrollListener(this@MangaListFragment)
} }
@@ -147,7 +151,7 @@ abstract class MangaListFragment :
override fun onReadClick(manga: Manga, view: View) { override fun onReadClick(manga: Manga, view: View) {
if (selectionController?.onItemClick(manga.id) != true) { if (selectionController?.onItemClick(manga.id) != true) {
val intent = IntentBuilder(view.context).manga(manga).build() val intent = IntentBuilder(view.context).manga(manga).build()
startActivity(intent, scaleUpActivityOptionsOf(view)) startActivity(intent)
} }
} }
@@ -195,14 +199,16 @@ abstract class MangaListFragment :
coil = coil, coil = coil,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
listener = this, listener = this,
sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = false),
) )
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
requireViewBinding().recyclerView.updatePadding( val rv = requireViewBinding().recyclerView
bottom = insets.bottom, rv.updatePadding(
bottom = insets.bottom + rv.paddingTop,
) )
requireViewBinding().recyclerView.fastScroller.updateLayoutParams<MarginLayoutParams> { rv.fastScroller.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.bottom bottomMargin = insets.bottom
} }
if (activity is MainActivity) { if (activity is MainActivity) {
@@ -241,21 +247,16 @@ abstract class MangaListFragment :
when (mode) { when (mode) {
ListMode.LIST -> { ListMode.LIST -> {
layoutManager = FitHeightLinearLayoutManager(context) layoutManager = FitHeightLinearLayoutManager(context)
updatePadding(left = 0, right = 0)
} }
ListMode.DETAILED_LIST -> { ListMode.DETAILED_LIST -> {
layoutManager = FitHeightLinearLayoutManager(context) layoutManager = FitHeightLinearLayoutManager(context)
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
updatePadding(left = spacing, right = spacing)
} }
ListMode.GRID -> { ListMode.GRID -> {
layoutManager = FitHeightGridLayoutManager(context, checkNotNull(spanResolver).spanCount).also { layoutManager = FitHeightGridLayoutManager(context, checkNotNull(spanResolver).spanCount).also {
it.spanSizeLookup = spanSizeLookup it.spanSizeLookup = spanSizeLookup
} }
val spacing = resources.getDimensionPixelOffset(R.dimen.grid_spacing)
updatePadding(left = spacing, right = spacing)
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
} }
} }
@@ -283,7 +284,7 @@ abstract class MangaListFragment :
} }
R.id.action_favourite -> { R.id.action_favourite -> {
FavouriteCategoriesSheet.show(childFragmentManager, selectedItems) FavouriteSheet.show(childFragmentManager, selectedItems)
mode.finish() mode.finish()
true true
} }

View File

@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
abstract class MangaListViewModel( abstract class MangaListViewModel(
settings: AppSettings, private val settings: AppSettings,
private val downloadScheduler: DownloadWorker.Scheduler, private val downloadScheduler: DownloadWorker.Scheduler,
) : BaseViewModel() { ) : BaseViewModel() {
@@ -46,4 +46,10 @@ abstract class MangaListViewModel(
onDownloadStarted.call(Unit) onDownloadStarted.call(Unit)
} }
} }
fun List<Manga>.skipNsfwIfNeeded() = if (settings.isNsfwContentDisabled) {
filterNot { it.isNsfw }
} else {
this
}
} }

View File

@@ -8,6 +8,7 @@ enum class ListItemType {
MANGA_LIST, MANGA_LIST,
MANGA_LIST_DETAILED, MANGA_LIST_DETAILED,
MANGA_GRID, MANGA_GRID,
MANGA_NESTED_GROUP,
FOOTER_LOADING, FOOTER_LOADING,
FOOTER_ERROR, FOOTER_ERROR,
STATE_LOADING, STATE_LOADING,
@@ -22,4 +23,6 @@ enum class ListItemType {
PAGE_THUMB, PAGE_THUMB,
FEED, FEED,
DOWNLOAD, DOWNLOAD,
CATEGORY_LARGE,
MANGA_SCROBBLING,
} }

View File

@@ -23,7 +23,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
fun mangaGridItemAD( fun mangaGridItemAD(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
sizeResolver: ItemSizeResolver?, sizeResolver: ItemSizeResolver,
clickListener: OnListItemClickListener<Manga>, clickListener: OnListItemClickListener<Manga>,
) = adapterDelegateViewBinding<MangaGridModel, ListModel, ItemMangaGridBinding>( ) = adapterDelegateViewBinding<MangaGridModel, ListModel, ItemMangaGridBinding>(
{ inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) },
@@ -36,7 +36,7 @@ fun mangaGridItemAD(
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it) clickListener.onItemLongClick(item.manga, it)
} }
sizeResolver?.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView) sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView)
bind { payloads -> bind { payloads ->
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
@@ -48,6 +48,7 @@ fun mangaGridItemAD(
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
transformations(TrimTransformation()) transformations(TrimTransformation())
allowRgb565(true) allowRgb565(true)
tag(item.manga)
source(item.source) source(item.source)
enqueueWith(coil) enqueueWith(coil)
} }

View File

@@ -4,22 +4,25 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
open class MangaListAdapter( open class MangaListAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: MangaListListener, listener: MangaListListener,
sizeResolver: ItemSizeResolver,
) : BaseListAdapter<ListModel>() { ) : BaseListAdapter<ListModel>() {
init { init {
addDelegate(ListItemType.MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, null, listener)) addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener)) addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener)) addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener)) addDelegate(ListItemType.HEADER, listHeaderAD(listener))
} }
} }

View File

@@ -61,6 +61,7 @@ fun mangaListDetailedItemAD(
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
transformations(TrimTransformation()) transformations(TrimTransformation())
allowRgb565(true) allowRgb565(true)
tag(item.manga)
source(item.source) source(item.source)
enqueueWith(coil) enqueueWith(coil)
} }

View File

@@ -42,6 +42,7 @@ fun mangaListItemAD(
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
transformations(TrimTransformation()) transformations(TrimTransformation())
tag(item.manga)
source(item.source) source(item.source)
enqueueWith(coil) enqueueWith(coil)
} }

View File

@@ -9,10 +9,12 @@ import org.koitharu.kotatsu.R
class TypedListSpacingDecoration( class TypedListSpacingDecoration(
context: Context, context: Context,
private val addHorizontalPadding: Boolean,
) : ItemDecoration() { ) : ItemDecoration() {
private val spacingList = context.resources.getDimensionPixelOffset(R.dimen.list_spacing) private val spacingSmall = context.resources.getDimensionPixelOffset(R.dimen.list_spacing_small)
private val spacingGrid = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing) private val spacingNormal = context.resources.getDimensionPixelOffset(R.dimen.list_spacing_normal)
private val spacingLarge = context.resources.getDimensionPixelOffset(R.dimen.list_spacing_large)
override fun getItemOffsets( override fun getItemOffsets(
outRect: Rect, outRect: Rect,
@@ -21,36 +23,51 @@ class TypedListSpacingDecoration(
state: RecyclerView.State state: RecyclerView.State
) { ) {
val itemType = parent.getChildViewHolder(view)?.itemViewType?.let { val itemType = parent.getChildViewHolder(view)?.itemViewType?.let {
ListItemType.values().getOrNull(it) ListItemType.entries.getOrNull(it)
} }
when (itemType) { when (itemType) {
ListItemType.FILTER_SORT, ListItemType.FILTER_SORT,
ListItemType.FILTER_TAG -> outRect.set(0) ListItemType.FILTER_TAG -> outRect.set(0)
ListItemType.HEADER -> outRect.set(spacingList, 0, spacingList, 0) ListItemType.HEADER,
ListItemType.FEED,
ListItemType.EXPLORE_SOURCE_LIST,
ListItemType.MANGA_SCROBBLING,
ListItemType.MANGA_LIST -> outRect.set(0) ListItemType.MANGA_LIST -> outRect.set(0)
ListItemType.DOWNLOAD, ListItemType.DOWNLOAD,
ListItemType.MANGA_LIST_DETAILED -> outRect.set(spacingList) ListItemType.HINT_EMPTY,
ListItemType.MANGA_LIST_DETAILED -> outRect.set(spacingNormal)
ListItemType.PAGE_THUMB, ListItemType.PAGE_THUMB,
ListItemType.MANGA_GRID -> outRect.set(spacingGrid) ListItemType.MANGA_GRID -> outRect.set(spacingNormal)
ListItemType.EXPLORE_BUTTONS -> outRect.set(spacingNormal)
ListItemType.FOOTER_LOADING, ListItemType.FOOTER_LOADING,
ListItemType.FOOTER_ERROR, ListItemType.FOOTER_ERROR,
ListItemType.STATE_LOADING, ListItemType.STATE_LOADING,
ListItemType.STATE_ERROR, ListItemType.STATE_ERROR,
ListItemType.STATE_EMPTY, ListItemType.STATE_EMPTY,
ListItemType.EXPLORE_BUTTONS,
ListItemType.EXPLORE_SOURCE_GRID, ListItemType.EXPLORE_SOURCE_GRID,
ListItemType.EXPLORE_SOURCE_LIST,
ListItemType.EXPLORE_SUGGESTION, ListItemType.EXPLORE_SUGGESTION,
ListItemType.MANGA_NESTED_GROUP,
ListItemType.CATEGORY_LARGE,
null -> outRect.set(0) null -> outRect.set(0)
ListItemType.TIP -> outRect.set(0) // TODO ListItemType.TIP -> outRect.set(0) // TODO
ListItemType.HINT_EMPTY -> outRect.set(0) // TODO }
ListItemType.FEED -> outRect.set(0) // TODO if (addHorizontalPadding && !itemType.isEdgeToEdge()) {
outRect.set(
outRect.left + spacingNormal,
outRect.top,
outRect.right + spacingNormal,
outRect.bottom,
)
} }
} }
private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing) private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing)
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
} }

View File

@@ -1,17 +1,14 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.ifZero import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.net.SocketTimeoutException
import java.net.UnknownHostException
suspend fun Manga.toListModel( suspend fun Manga.toListModel(
extraProvider: ListExtraProvider? extraProvider: ListExtraProvider?
@@ -79,7 +76,7 @@ suspend fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi(
fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState( fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(
exception = this, exception = this,
icon = getErrorIcon(this), icon = getDisplayIcon(),
canRetry = canRetry, canRetry = canRetry,
buttonText = ExceptionResolver.getResolveStringId(this).ifZero { R.string.try_again }, buttonText = ExceptionResolver.getResolveStringId(this).ifZero { R.string.try_again },
) )
@@ -88,13 +85,3 @@ fun Throwable.toErrorFooter() = ErrorFooter(
exception = this, exception = this,
icon = R.drawable.ic_alert_outline, icon = R.drawable.ic_alert_outline,
) )
private fun getErrorIcon(error: Throwable) = when (error) {
is AuthRequiredException -> R.drawable.ic_auth_key_large
is CloudFlareProtectedException -> R.drawable.ic_bot_large
is UnknownHostException,
is SocketTimeoutException,
-> R.drawable.ic_plug_large
else -> R.drawable.ic_error_large
}

View File

@@ -69,7 +69,6 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
R.id.button_close -> closeSelf() R.id.button_close -> closeSelf()
R.id.button_open -> startActivity( R.id.button_open -> startActivity(
DetailsActivity.newIntent(v.context, manga), DetailsActivity.newIntent(v.context, manga),
scaleUpActivityOptionsOf(requireView()),
) )
R.id.textView_author -> startActivity( R.id.textView_author -> startActivity(

View File

@@ -5,7 +5,6 @@ import android.content.res.Resources
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.core.view.updateLayoutParams
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -14,7 +13,11 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.ui.util.ReadingProgressView import org.koitharu.kotatsu.history.ui.util.ReadingProgressView
import kotlin.math.roundToInt import kotlin.math.roundToInt
class DynamicItemSizeResolver(resources: Resources, private val settings: AppSettings) : ItemSizeResolver { class DynamicItemSizeResolver(
resources: Resources,
private val settings: AppSettings,
private val adjustWidth: Boolean,
) : ItemSizeResolver {
private val gridWidth = resources.getDimension(R.dimen.preferred_grid_width) private val gridWidth = resources.getDimension(R.dimen.preferred_grid_width)
private val scaleFactor: Float private val scaleFactor: Float
@@ -72,8 +75,12 @@ class DynamicItemSizeResolver(resources: Resources, private val settings: AppSet
fun update() { fun update() {
val newWidth = cellWidth val newWidth = cellWidth
textView?.adjustTextAppearance(newWidth) textView?.adjustTextAppearance(newWidth)
view.updateLayoutParams { if (adjustWidth) {
width = newWidth val lp = view.layoutParams
if (lp.width != newWidth) {
lp.width = newWidth
view.layoutParams = lp
}
} }
progressView?.adjustSize(newWidth) progressView?.adjustSize(newWidth)
} }

View File

@@ -15,7 +15,9 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeMutex import org.koitharu.kotatsu.core.util.CompositeMutex
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.filterWith
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
@@ -127,10 +129,8 @@ class LocalMangaRepository @Inject constructor(
} }
suspend fun findSavedManga(remoteManga: Manga): LocalManga? { suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
// TODO fast path by name
val files = getAllFiles() val files = getAllFiles()
if (files.isEmpty()) {
return null
}
return channelFlow { return channelFlow {
for (file in files) { for (file in files) {
launch { launch {
@@ -172,7 +172,7 @@ class LocalMangaRepository @Inject constructor(
val dirs = storageManager.getWriteableDirs() val dirs = storageManager.getWriteableDirs()
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
dirs.flatMap { dir -> dirs.flatMap { dir ->
dir.listFiles(TempFileFilter())?.toList().orEmpty() dir.children().filterWith(TempFileFilter())
}.forEach { file -> }.forEach { file ->
file.deleteRecursively() file.deleteRecursively()
} }
@@ -189,7 +189,7 @@ class LocalMangaRepository @Inject constructor(
} }
private suspend fun getRawList(): ArrayList<LocalManga> { private suspend fun getRawList(): ArrayList<LocalManga> {
val files = getAllFiles() val files = getAllFiles().toList() // TODO remove toList()
return coroutineScope { return coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
files.map { file -> files.map { file ->
@@ -200,8 +200,8 @@ class LocalMangaRepository @Inject constructor(
}.filterNotNullTo(ArrayList(files.size)) }.filterNotNullTo(ArrayList(files.size))
} }
private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> private suspend fun getAllFiles() = storageManager.getReadableDirs().asSequence().flatMap { dir ->
dir.listFiles()?.toList().orEmpty() dir.children()
} }
private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga } private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }

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