Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4502ffb6d2 | ||
|
|
b6f9ce824e | ||
|
|
d33081c1c7 | ||
|
|
76c08535d6 | ||
|
|
b55fef67e1 | ||
|
|
56798677d5 | ||
|
|
ff30b9c225 | ||
|
|
5c3293ec44 | ||
|
|
1b17605e0e | ||
|
|
ba4e4dcf56 | ||
|
|
b35d5d4779 | ||
|
|
124f31ebe1 | ||
|
|
173087ee19 | ||
|
|
8d7bad97de | ||
|
|
188fbfbb95 | ||
|
|
3498a54bdf | ||
|
|
18169c2355 | ||
|
|
87beb9442f | ||
|
|
e642d54929 | ||
|
|
59ce5d5e67 | ||
|
|
58d5237692 | ||
|
|
8d5bde6e60 | ||
|
|
bf740ddc93 | ||
|
|
fddbf35e8c | ||
|
|
a47fea02d1 | ||
|
|
250136cfdc | ||
|
|
597ad01e8f | ||
|
|
f7b44f2b0f | ||
|
|
5aab43ac93 | ||
|
|
2d278159ea | ||
|
|
da61462d79 | ||
|
|
2ab0912880 | ||
|
|
3914616222 | ||
|
|
a73b2703be | ||
|
|
49590f6d02 | ||
|
|
f4a0fcf5ba | ||
|
|
6ab803e682 | ||
|
|
0faa97b08c | ||
|
|
2ae488544b | ||
|
|
a7e2cfc878 | ||
|
|
da6db9c1b4 | ||
|
|
88b3e5cf34 | ||
|
|
7347f0ba10 | ||
|
|
4c55682552 | ||
|
|
324031aa2a | ||
|
|
1355c3d75c | ||
|
|
8533168155 | ||
|
|
51f6ec6e55 | ||
|
|
7e3f67c14d | ||
|
|
c51320f033 | ||
|
|
9c50a47abc | ||
|
|
473d273d18 | ||
|
|
f19b628655 | ||
|
|
fa74d4b27a | ||
|
|
cdb6655e37 | ||
|
|
4f19f7ebdf | ||
|
|
bf8838f943 | ||
|
|
1e1e9fabdc | ||
|
|
745972a717 | ||
|
|
6055776329 | ||
|
|
4074791f9a | ||
|
|
b1ab48e912 | ||
|
|
a71e2dd289 | ||
|
|
b8283acd0d | ||
|
|
bbdf1c756e | ||
|
|
283878879b | ||
|
|
b74ec98d68 | ||
|
|
3691db8e8e | ||
|
|
e25ccf6b25 | ||
|
|
ffebdb0c49 | ||
|
|
6accdbced5 | ||
|
|
2fcb94e1d7 | ||
|
|
6211ef974d | ||
|
|
0eacf7bb98 | ||
|
|
c9b7d650a8 | ||
|
|
a29f7d6533 | ||
|
|
72f8c626d7 | ||
|
|
f05ef5125d | ||
|
|
40b3d8e6fd | ||
|
|
a695bdc565 | ||
|
|
9700fabd9a | ||
|
|
4877db42f9 | ||
|
|
9b418fd63b | ||
|
|
b2eef0df11 | ||
|
|
34462829ff | ||
|
|
2afcbef8d0 | ||
|
|
695becbda0 | ||
|
|
5877d8215d | ||
|
|
48b357dfef | ||
|
|
b20cc7c0d9 | ||
|
|
0f43f02fad | ||
|
|
9b658cf0b8 | ||
|
|
ce705e12a8 | ||
|
|
28dede0d3e | ||
|
|
d66e61f845 | ||
|
|
b246575486 | ||
|
|
18dd205051 | ||
|
|
0e10fdaf36 | ||
|
|
7c82b4effb | ||
|
|
82684601b7 | ||
|
|
77ad21bd7a | ||
|
|
e6c8591bf8 | ||
|
|
e330be5d13 | ||
|
|
6a4cd9643a | ||
|
|
d98cb9a577 | ||
|
|
ac455527ef | ||
|
|
7e37345dea | ||
|
|
6e810179a7 | ||
|
|
7715aff953 | ||
|
|
63e6b9f026 | ||
|
|
b6f136fb71 | ||
|
|
de0327a00a | ||
|
|
e5f09ae4c9 | ||
|
|
f10d9b54d8 | ||
|
|
619d672e49 | ||
|
|
db519701bc | ||
|
|
e42aeb857f | ||
|
|
4f82495cfc | ||
|
|
311c36b7c0 | ||
|
|
002ce25d7e | ||
|
|
d9cf13d3fb | ||
|
|
ed5b1306b8 | ||
|
|
227fe86cf9 | ||
|
|
1905482b06 | ||
|
|
46ded4af0d | ||
|
|
6676ab82b4 | ||
|
|
1a60df6d98 | ||
|
|
5ef1b4ac9c | ||
|
|
17828ae755 | ||
|
|
d8ac4d6738 | ||
|
|
0a10cb509c | ||
|
|
7a3fd20dfa | ||
|
|
ab20e50dc1 | ||
|
|
f783ffef11 | ||
|
|
e01c485949 | ||
|
|
3672c84e8f | ||
|
|
55c5a07c8b | ||
|
|
a3cf32aefb | ||
|
|
c21bf30e91 | ||
|
|
1719547ce0 | ||
|
|
22186825a0 | ||
|
|
b9c83ad5cc | ||
|
|
1359689b23 | ||
|
|
7bad6ad077 | ||
|
|
b9097fa077 | ||
|
|
0b03806ccd | ||
|
|
db9c1279ac |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,4 +2,4 @@ blank_issues_enabled: false
|
|||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠️ Source issue
|
- name: ⚠️ Source issue
|
||||||
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
|
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
|
||||||
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead
|
about: If you have a problem with a specific **manga source** or want to propose a new one, please open an issue in the kotatsu-parsers repository instead
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
@@ -60,7 +60,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
|
||||||
required: true
|
required: true
|
||||||
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
- label: This is not an issue with a specific manga source. Otherwise, you have to open an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -20,5 +20,5 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@
|
|||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
|
/.idea/deviceManager.xml
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 636
|
versionCode = 648
|
||||||
versionName = '7.0-b2'
|
versionName = '7.2'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -82,31 +82,32 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:a245574dee') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:56fd22b43f') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'androidx.core:core-ktx:1.13.0'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.9.0'
|
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-ktx:1.7.1'
|
||||||
|
implementation 'androidx.transition:transition-ktx:1.5.0'
|
||||||
implementation 'androidx.collection:collection-ktx:1.4.0'
|
implementation 'androidx.collection:collection-ktx:1.4.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-service:2.8.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-process:2.8.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
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.12.0-rc01'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.1'
|
||||||
implementation 'androidx.webkit:webkit:1.10.0'
|
implementation 'androidx.webkit:webkit:1.11.0'
|
||||||
|
|
||||||
implementation 'androidx.work:work-runtime:2.9.0'
|
implementation 'androidx.work:work-runtime:2.9.0'
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
@@ -121,6 +122,7 @@ dependencies {
|
|||||||
ksp 'androidx.room:room-compiler:2.6.1'
|
ksp 'androidx.room:room-compiler:2.6.1'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
||||||
implementation 'com.squareup.okio:okio:3.9.0'
|
implementation 'com.squareup.okio:okio:3.9.0'
|
||||||
|
|
||||||
@@ -146,17 +148,18 @@ dependencies {
|
|||||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||||
|
debugImplementation 'com.github.Koitharu:WorkInspector:5778dd1747'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20240303'
|
testImplementation 'org.json:json:20240303'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.6.1'
|
androidTestImplementation 'androidx.room:room-testing:2.6.1'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<manifest
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".tracker.ui.debug.TrackerDebugActivity"
|
|
||||||
android:label="@string/check_for_new_chapters" />
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.reader.domain.PageLoader
|
|||||||
|
|
||||||
class KotatsuApp : BaseApp() {
|
class KotatsuApp : BaseApp() {
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
enableStrictMode()
|
enableStrictMode()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import android.os.Looper
|
||||||
|
|
||||||
|
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||||
|
|
||||||
|
fun assertNotInMainThread() = check(Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
|
"Calling this from the main thread is prohibited"
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
|
||||||
|
|
||||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.koitharu.kotatsu.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import leakcanary.LeakCanary
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.workinspector.WorkInspector
|
||||||
|
|
||||||
|
class SettingsMenuProvider(
|
||||||
|
private val context: Context,
|
||||||
|
) : MenuProvider {
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.opt_settings, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
|
R.id.action_leaks -> {
|
||||||
|
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.action_works -> {
|
||||||
|
context.startActivity(WorkInspector.getIntent(context))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@id/action_tracker"
|
android:id="@id/action_works"
|
||||||
android:title="@string/check_for_new_chapters"
|
android:title="@string/wi_lib_name"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
||||||
|
<bool name="wi_launcher_icon_enabled" tools:node="replace">false</bool>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -100,6 +100,13 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="${applicationId}.action.READ_MANGA" />
|
<action android:name="${applicationId}.action.READ_MANGA" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
|
android:resource="@xml/remote_action" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||||
@@ -122,7 +129,7 @@
|
|||||||
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
||||||
android:label="@string/favourites" />
|
android:label="@string/favourites" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
|
android:name="org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity"
|
||||||
android:label="@string/bookmarks" />
|
android:label="@string/bookmarks" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
||||||
@@ -245,6 +252,12 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity"
|
android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity"
|
||||||
android:label="@string/alternatives" />
|
android:label="@string/alternatives" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.settings.about.AppUpdateActivity"
|
||||||
|
android:label="@string/app_update_available" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity"
|
||||||
|
android:label="@string/tracker_debug_info" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
@@ -357,13 +370,6 @@
|
|||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/widget_recent" />
|
android:resource="@xml/widget_recent" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
|
||||||
android:name="org.koitharu.kotatsu.settings.about.UpdateDownloadReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver"
|
android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
|||||||
31
app/src/main/assets/isrgrootx1.pem
Normal file
31
app/src/main/assets/isrgrootx1.pem
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||||
|
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||||
|
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||||
|
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||||
|
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||||
|
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||||
|
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||||
|
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||||
|
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||||
|
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||||
|
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||||
|
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||||
|
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||||
|
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||||
|
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||||
|
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||||
|
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||||
|
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||||
|
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||||
|
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||||
|
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||||
|
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||||
|
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||||
|
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||||
|
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||||
|
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.history.data.toMangaHistory
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MigrateUseCase @Inject constructor(
|
class MigrateUseCase @Inject constructor(
|
||||||
@@ -56,6 +57,23 @@ class MigrateUseCase @Inject constructor(
|
|||||||
historyDao.delete(oldDetails.id)
|
historyDao.delete(oldDetails.id)
|
||||||
historyDao.upsert(newHistory)
|
historyDao.upsert(newHistory)
|
||||||
}
|
}
|
||||||
|
// track
|
||||||
|
val tracksDao = database.getTracksDao()
|
||||||
|
val oldTrack = tracksDao.find(oldDetails.id)
|
||||||
|
if (oldTrack != null) {
|
||||||
|
val lastChapter = newDetails.chapters?.lastOrNull()
|
||||||
|
val newTrack = TrackEntity(
|
||||||
|
mangaId = newDetails.id,
|
||||||
|
lastChapterId = lastChapter?.id ?: 0L,
|
||||||
|
newChapters = 0,
|
||||||
|
lastCheckTime = System.currentTimeMillis(),
|
||||||
|
lastChapterDate = lastChapter?.uploadDate ?: 0L,
|
||||||
|
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
||||||
|
lastError = null,
|
||||||
|
)
|
||||||
|
tracksDao.delete(oldDetails.id)
|
||||||
|
tracksDao.upsert(newTrack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
progressUpdateUseCase(newManga)
|
progressUpdateUseCase(newManga)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
|||||||
@Dao
|
@Dao
|
||||||
abstract class BookmarksDao {
|
abstract class BookmarksDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
|
||||||
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
|
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
|
||||||
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
||||||
|
|
||||||
@@ -42,9 +39,6 @@ abstract class BookmarksDao {
|
|||||||
@Delete
|
@Delete
|
||||||
abstract suspend fun delete(entity: BookmarkEntity)
|
abstract suspend fun delete(entity: BookmarkEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
|
||||||
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
|
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
|
||||||
abstract suspend fun delete(pageId: Long): Int
|
abstract suspend fun delete(pageId: Long): Int
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
|||||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BookmarksActivity :
|
class AllBookmarksActivity :
|
||||||
BaseActivity<ActivityContainerBinding>(),
|
BaseActivity<ActivityContainerBinding>(),
|
||||||
AppBarOwner,
|
AppBarOwner,
|
||||||
SnackbarOwner {
|
SnackbarOwner {
|
||||||
@@ -35,7 +35,7 @@ class BookmarksActivity :
|
|||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
setReorderingAllowed(true)
|
setReorderingAllowed(true)
|
||||||
replace(R.id.container, BookmarksFragment::class.java, null)
|
replace(R.id.container, AllBookmarksFragment::class.java, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +49,6 @@ class BookmarksActivity :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java)
|
fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ import coil.ImageLoader
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
|
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||||
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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
@@ -42,7 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BookmarksFragment :
|
class AllBookmarksFragment :
|
||||||
BaseFragment<FragmentListSimpleBinding>(),
|
BaseFragment<FragmentListSimpleBinding>(),
|
||||||
ListStateHolderListener,
|
ListStateHolderListener,
|
||||||
OnListItemClickListener<Bookmark>,
|
OnListItemClickListener<Bookmark>,
|
||||||
@@ -55,7 +55,7 @@ class BookmarksFragment :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
|
|
||||||
private val viewModel by viewModels<BookmarksViewModel>()
|
private val viewModel by viewModels<AllBookmarksViewModel>()
|
||||||
private var bookmarksAdapter: BookmarksAdapter? = null
|
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||||
private var selectionController: ListSelectionController? = null
|
private var selectionController: ListSelectionController? = null
|
||||||
|
|
||||||
@@ -213,6 +213,6 @@ class BookmarksFragment :
|
|||||||
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
|
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
fun newInstance() = BookmarksFragment()
|
fun newInstance() = AllBookmarksFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BookmarksViewModel @Inject constructor(
|
class AllBookmarksViewModel @Inject constructor(
|
||||||
private val repository: BookmarksRepository,
|
private val repository: BookmarksRepository,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
@@ -1,19 +1,36 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
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.core.ui.list.fastscroll.FastScroller
|
||||||
|
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.loadingFooterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
class BookmarksAdapter(
|
class BookmarksAdapter(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
clickListener: OnListItemClickListener<Bookmark>,
|
clickListener: OnListItemClickListener<Bookmark>,
|
||||||
) : BaseListAdapter<Bookmark>() {
|
headerClickListener: ListHeaderClickListener?,
|
||||||
|
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener))
|
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
|
||||||
|
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
|
||||||
|
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||||
|
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||||
|
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
|
return findHeader(position)?.getText(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import coil.ImageLoader
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
|
||||||
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.loadingFooterAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
|
|
||||||
class BookmarksAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
clickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
headerClickListener: ListHeaderClickListener?,
|
|
||||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
|
||||||
|
|
||||||
init {
|
|
||||||
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
|
|
||||||
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
|
|
||||||
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
|
||||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
|
||||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
|
||||||
return findHeader(position)?.getText(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -108,8 +108,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
viewBinding.webView.stopLoading()
|
if (hasViewBinding()) {
|
||||||
viewBinding.webView.destroy()
|
viewBinding.webView.stopLoading()
|
||||||
|
viewBinding.webView.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
@@ -33,7 +36,7 @@ class CaptchaNotifier(
|
|||||||
.build()
|
.build()
|
||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
|
|
||||||
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers)
|
val intent = CloudFlareActivity.newIntent(context, exception)
|
||||||
.setData(exception.url.toUri())
|
.setData(exception.url.toUri())
|
||||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setContentTitle(channel.name)
|
.setContentTitle(channel.name)
|
||||||
@@ -56,8 +59,21 @@ class CaptchaNotifier(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
|
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
|
||||||
.build()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
manager.notify(TAG, exception.source.hashCode(), notification)
|
val actionIntent = PendingIntentCompat.getActivity(
|
||||||
|
context, SETTINGS_ACTION_CODE,
|
||||||
|
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||||
|
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||||
|
.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID),
|
||||||
|
0, false,
|
||||||
|
)
|
||||||
|
notification.addAction(
|
||||||
|
R.drawable.ic_settings,
|
||||||
|
context.getString(R.string.notifications_settings),
|
||||||
|
actionIntent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
manager.notify(TAG, exception.source.hashCode(), notification.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismiss(source: MangaSource) {
|
fun dismiss(source: MangaSource) {
|
||||||
@@ -84,5 +100,6 @@ class CaptchaNotifier(
|
|||||||
private const val CHANNEL_ID = "captcha"
|
private const val CHANNEL_ID = "captcha"
|
||||||
private const val TAG = CHANNEL_ID
|
private const val TAG = CHANNEL_ID
|
||||||
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"
|
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"
|
||||||
|
private const val SETTINGS_ACTION_CODE = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ import okhttp3.HttpUrl
|
|||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
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.exceptions.CloudFlareProtectedException
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
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
|
||||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@@ -137,6 +140,10 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
|
|
||||||
override fun onCheckPassed() {
|
override fun onCheckPassed() {
|
||||||
pendingResult = RESULT_OK
|
pendingResult = RESULT_OK
|
||||||
|
val source = intent?.getStringExtra(ARG_SOURCE)
|
||||||
|
if (source != null) {
|
||||||
|
CaptchaNotifier(this).dismiss(MangaSource(source))
|
||||||
|
}
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +181,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
class Contract : ActivityResultContract<CloudFlareProtectedException, TaggedActivityResult>() {
|
||||||
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent {
|
override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
|
||||||
return newIntent(context, input.first, input.second)
|
return newIntent(context, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||||
@@ -188,13 +195,23 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
|
|
||||||
const val TAG = "CloudFlareActivity"
|
const val TAG = "CloudFlareActivity"
|
||||||
private const val ARG_UA = "ua"
|
private const val ARG_UA = "ua"
|
||||||
|
private const val ARG_SOURCE = "_source"
|
||||||
|
|
||||||
fun newIntent(
|
fun newIntent(context: Context, exception: CloudFlareProtectedException) = newIntent(
|
||||||
|
context = context,
|
||||||
|
url = exception.url,
|
||||||
|
source = exception.source,
|
||||||
|
headers = exception.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun newIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
url: String,
|
url: String,
|
||||||
|
source: MangaSource?,
|
||||||
headers: Headers?,
|
headers: Headers?,
|
||||||
) = Intent(context, CloudFlareActivity::class.java).apply {
|
) = Intent(context, CloudFlareActivity::class.java).apply {
|
||||||
data = url.toUri()
|
data = url.toUri()
|
||||||
|
putExtra(ARG_SOURCE, source?.name)
|
||||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||||
putExtra(ARG_UA, it)
|
putExtra(ARG_UA, it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,34 +26,35 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
|
||||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
|
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||||
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
||||||
|
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
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.CoverRestoreInterceptor
|
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
|
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||||
|
import javax.inject.Provider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -71,8 +72,9 @@ interface AppModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideNetworkState(
|
fun provideNetworkState(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context,
|
||||||
) = NetworkState(context.connectivityManager)
|
settings: AppSettings,
|
||||||
|
) = NetworkState(context.connectivityManager, settings)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -86,7 +88,7 @@ interface AppModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideCoil(
|
fun provideCoil(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
imageProxyInterceptor: ImageProxyInterceptor,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
@@ -98,11 +100,14 @@ interface AppModule {
|
|||||||
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
|
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
val okHttpClientLazy = lazy {
|
||||||
|
okHttpClientProvider.get().newBuilder().cache(null).build()
|
||||||
|
}
|
||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
.okHttpClient(okHttpClient.newBuilder().cache(null).build())
|
.okHttpClient { okHttpClientLazy.value }
|
||||||
.interceptorDispatcher(Dispatchers.Default)
|
.interceptorDispatcher(Dispatchers.Default)
|
||||||
.fetcherDispatcher(Dispatchers.IO)
|
.fetcherDispatcher(Dispatchers.Default)
|
||||||
.decoderDispatcher(Dispatchers.Default)
|
.decoderDispatcher(Dispatchers.IO)
|
||||||
.transformationDispatcher(Dispatchers.Default)
|
.transformationDispatcher(Dispatchers.Default)
|
||||||
.diskCache(diskCacheFactory)
|
.diskCache(diskCacheFactory)
|
||||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
@@ -112,7 +117,8 @@ interface AppModule {
|
|||||||
ComponentRegistry.Builder()
|
ComponentRegistry.Builder()
|
||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
.add(CbzFetcher.Factory())
|
.add(CbzFetcher.Factory())
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
|
||||||
|
.add(MangaPageKeyer())
|
||||||
.add(pageFetcherFactory)
|
.add(pageFetcherFactory)
|
||||||
.add(imageProxyInterceptor)
|
.add(imageProxyInterceptor)
|
||||||
.add(coverRestoreInterceptor)
|
.add(coverRestoreInterceptor)
|
||||||
@@ -147,24 +153,14 @@ interface AppModule {
|
|||||||
appProtectHelper: AppProtectHelper,
|
appProtectHelper: AppProtectHelper,
|
||||||
activityRecreationHandle: ActivityRecreationHandle,
|
activityRecreationHandle: ActivityRecreationHandle,
|
||||||
acraScreenLogger: AcraScreenLogger,
|
acraScreenLogger: AcraScreenLogger,
|
||||||
|
screenshotPolicyHelper: ScreenshotPolicyHelper,
|
||||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||||
appProtectHelper,
|
appProtectHelper,
|
||||||
activityRecreationHandle,
|
activityRecreationHandle,
|
||||||
acraScreenLogger,
|
acraScreenLogger,
|
||||||
|
screenshotPolicyHelper,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideContentCache(
|
|
||||||
application: Application,
|
|
||||||
): ContentCache {
|
|
||||||
return if (application.isLowRamDevice()) {
|
|
||||||
StubContentCache()
|
|
||||||
} else {
|
|
||||||
MemoryContentCache(application)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@LocalStorageChanges
|
@LocalStorageChanges
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import javax.inject.Provider
|
|||||||
open class BaseApp : Application(), Configuration.Provider {
|
open class BaseApp : Application(), Configuration.Provider {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
|
lateinit var databaseObserversProvider: Provider<Set<@JvmSuppressWildcards InvalidationTracker.Observer>>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
|
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
|
||||||
@@ -87,7 +87,7 @@ open class BaseApp : Application(), Configuration.Provider {
|
|||||||
WorkServiceStopHelper(workManagerProvider).setup()
|
WorkServiceStopHelper(workManagerProvider).setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
initAcra {
|
initAcra {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
@@ -123,7 +123,7 @@ open class BaseApp : Application(), Configuration.Provider {
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun setupDatabaseObservers() {
|
private fun setupDatabaseObservers() {
|
||||||
val tracker = database.get().invalidationTracker
|
val tracker = database.get().invalidationTracker
|
||||||
databaseObservers.forEach {
|
databaseObserversProvider.get().forEach {
|
||||||
tracker.addObserver(it)
|
tracker.addObserver(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptibl
|
|||||||
val filename = buildString {
|
val filename = buildString {
|
||||||
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
|
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
|
||||||
append('_')
|
append('_')
|
||||||
append(LocalDate.now().format(DateTimeFormatter.ofPattern("ddMMyyyy")))
|
append(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
|
||||||
append(".bk.zip")
|
append(".bk.zip")
|
||||||
}
|
}
|
||||||
BackupZipOutput(File(dir, filename))
|
BackupZipOutput(File(dir, filename))
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class JsonDeserializer(private val json: JSONObject) {
|
|||||||
source = json.getString("source"),
|
source = json.getString("source"),
|
||||||
isEnabled = json.getBoolean("enabled"),
|
isEnabled = json.getBoolean("enabled"),
|
||||||
sortKey = json.getInt("sort_key"),
|
sortKey = json.getInt("sort_key"),
|
||||||
|
addedIn = json.getIntOrDefault("added_in", 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toMap(): Map<String, Any?> {
|
fun toMap(): Map<String, Any?> {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.cache
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
interface ContentCache {
|
|
||||||
|
|
||||||
val isCachingEnabled: Boolean
|
|
||||||
|
|
||||||
suspend fun getDetails(source: MangaSource, url: String): Manga?
|
|
||||||
|
|
||||||
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
|
|
||||||
|
|
||||||
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
|
|
||||||
|
|
||||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
|
||||||
|
|
||||||
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
|
|
||||||
|
|
||||||
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
|
|
||||||
|
|
||||||
fun clear(source: MangaSource)
|
|
||||||
|
|
||||||
data class Key(
|
|
||||||
val source: MangaSource,
|
|
||||||
val url: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,19 @@ package org.koitharu.kotatsu.core.cache
|
|||||||
|
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache.Key as CacheKey
|
||||||
|
|
||||||
class ExpiringLruCache<T>(
|
class ExpiringLruCache<T>(
|
||||||
val maxSize: Int,
|
val maxSize: Int,
|
||||||
private val lifetime: Long,
|
private val lifetime: Long,
|
||||||
private val timeUnit: TimeUnit,
|
private val timeUnit: TimeUnit,
|
||||||
) : Iterable<ContentCache.Key> {
|
) : Iterable<CacheKey> {
|
||||||
|
|
||||||
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
private val cache = LruCache<CacheKey, ExpiringValue<T>>(maxSize)
|
||||||
|
|
||||||
override fun iterator(): Iterator<ContentCache.Key> = cache.snapshot().keys.iterator()
|
override fun iterator(): Iterator<CacheKey> = cache.snapshot().keys.iterator()
|
||||||
|
|
||||||
operator fun get(key: ContentCache.Key): T? {
|
operator fun get(key: CacheKey): T? {
|
||||||
val value = cache[key] ?: return null
|
val value = cache[key] ?: return null
|
||||||
if (value.isExpired) {
|
if (value.isExpired) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
@@ -21,7 +22,7 @@ class ExpiringLruCache<T>(
|
|||||||
return value.get()
|
return value.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun set(key: ContentCache.Key, value: T) {
|
operator fun set(key: CacheKey, value: T) {
|
||||||
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class ExpiringLruCache<T>(
|
|||||||
cache.trimToSize(size)
|
cache.trimToSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(key: ContentCache.Key) {
|
fun remove(key: CacheKey) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,48 +3,54 @@ package org.koitharu.kotatsu.core.cache
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentCallbacks2
|
import android.content.ComponentCallbacks2
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
@Singleton
|
||||||
|
class MemoryContentCache @Inject constructor(application: Application) : ComponentCallbacks2 {
|
||||||
|
|
||||||
|
private val isLowRam = application.isLowRamDevice()
|
||||||
|
|
||||||
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
||||||
|
private val pagesCache =
|
||||||
|
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
|
||||||
|
private val relatedMangaCache =
|
||||||
|
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
application.registerComponentCallbacks(this)
|
application.registerComponentCallbacks(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||||
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
|
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = true
|
|
||||||
|
|
||||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
|
||||||
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||||
detailsCache[ContentCache.Key(source, url)] = details
|
detailsCache[Key(source, url)] = details
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||||
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
return pagesCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||||
pagesCache[ContentCache.Key(source, url)] = pages
|
pagesCache[Key(source, url)] = pages
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||||
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
return relatedMangaCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||||
relatedMangaCache[ContentCache.Key(source, url)] = related
|
relatedMangaCache[Key(source, url)] = related
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear(source: MangaSource) {
|
fun clear(source: MangaSource) {
|
||||||
clearCache(detailsCache, source)
|
clearCache(detailsCache, source)
|
||||||
clearCache(pagesCache, source)
|
clearCache(pagesCache, source)
|
||||||
clearCache(relatedMangaCache, source)
|
clearCache(relatedMangaCache, source)
|
||||||
@@ -81,4 +87,9 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Key(
|
||||||
|
val source: MangaSource,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.cache
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
class StubContentCache : ContentCache {
|
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = false
|
|
||||||
|
|
||||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
|
|
||||||
|
|
||||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
|
|
||||||
|
|
||||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
|
||||||
|
|
||||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
|
||||||
|
|
||||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? = null
|
|
||||||
|
|
||||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) = Unit
|
|
||||||
|
|
||||||
override fun clear(source: MangaSource) = Unit
|
|
||||||
}
|
|
||||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration17To18
|
|||||||
import org.koitharu.kotatsu.core.db.migrations.Migration18To19
|
import org.koitharu.kotatsu.core.db.migrations.Migration18To19
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
|
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||||
|
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
||||||
@@ -58,7 +59,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
|||||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||||
|
|
||||||
const val DATABASE_VERSION = 20
|
const val DATABASE_VERSION = 21
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
@@ -118,6 +119,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
|||||||
Migration17To18(),
|
Migration17To18(),
|
||||||
Migration18To19(),
|
Migration18To19(),
|
||||||
Migration19To20(),
|
Migration19To20(),
|
||||||
|
Migration20To21(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ abstract class MangaDao {
|
|||||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
abstract suspend fun upsert(manga: MangaEntity)
|
protected abstract suspend fun upsert(manga: MangaEntity)
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun update(manga: MangaEntity): Int
|
abstract suspend fun update(manga: MangaEntity): Int
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
|
|||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||||
|
|
||||||
@@ -20,11 +21,11 @@ abstract class MangaSourcesDao {
|
|||||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
abstract suspend fun findAll(): List<MangaSourceEntity>
|
abstract suspend fun findAll(): List<MangaSourceEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
@Query("SELECT source FROM sources WHERE enabled = 1")
|
||||||
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
|
abstract suspend fun findAllEnabledNames(): List<String>
|
||||||
|
|
||||||
@Query("SELECT * FROM sources WHERE enabled = 0")
|
@Query("SELECT * FROM sources WHERE added_in >= :version")
|
||||||
abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
|
abstract suspend fun findAllFromVersion(version: Int): List<MangaSourceEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||||
@@ -71,6 +72,7 @@ abstract class MangaSourcesDao {
|
|||||||
source = source,
|
source = source,
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
sortKey = getMaxSortKey() + 1,
|
sortKey = getMaxSortKey() + 1,
|
||||||
|
addedIn = BuildConfig.VERSION_CODE,
|
||||||
)
|
)
|
||||||
upsert(entity)
|
upsert(entity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ interface TrackLogsDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(entity: TrackLogEntity): Long
|
suspend fun insert(entity: TrackLogEntity): Long
|
||||||
|
|
||||||
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
|
|
||||||
suspend fun removeAll(mangaId: Long)
|
|
||||||
|
|
||||||
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
|
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
|
||||||
suspend fun gc()
|
suspend fun gc()
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ data class MangaSourceEntity(
|
|||||||
val source: String,
|
val source: String,
|
||||||
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
||||||
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
||||||
|
@ColumnInfo(name = "added_in") val addedIn: Int,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
class Migration20To21 : Migration(20, 21) {
|
||||||
|
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("ALTER TABLE tracks ADD COLUMN `last_error` TEXT DEFAULT NULL")
|
||||||
|
db.execSQL("ALTER TABLE sources ADD COLUMN `added_in` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,47 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions.resolve
|
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.MutableScatterMap
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import okhttp3.Headers
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity.BaseActivityEntryPoint
|
||||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||||
|
import java.security.cert.CertPathValidatorException
|
||||||
|
import javax.net.ssl.SSLException
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||||
|
|
||||||
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
|
private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1)
|
||||||
private val activity: FragmentActivity?
|
private val activity: FragmentActivity?
|
||||||
private val fragment: Fragment?
|
private val fragment: Fragment?
|
||||||
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
|
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
|
||||||
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>>
|
private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
|
||||||
|
|
||||||
|
val context: Context?
|
||||||
|
get() = activity ?: fragment?.context
|
||||||
|
|
||||||
constructor(activity: FragmentActivity) {
|
constructor(activity: FragmentActivity) {
|
||||||
this.activity = activity
|
this.activity = activity
|
||||||
@@ -55,8 +66,14 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
||||||
is CloudFlareProtectedException -> resolveCF(e.url, e.headers)
|
is CloudFlareProtectedException -> resolveCF(e)
|
||||||
is AuthRequiredException -> resolveAuthException(e.source)
|
is AuthRequiredException -> resolveAuthException(e.source)
|
||||||
|
is SSLException,
|
||||||
|
is CertPathValidatorException -> {
|
||||||
|
showSslErrorDialog()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
is NotFoundException -> {
|
is NotFoundException -> {
|
||||||
openInBrowser(e.url)
|
openInBrowser(e.url)
|
||||||
false
|
false
|
||||||
@@ -70,9 +87,9 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont ->
|
private suspend fun resolveCF(e: CloudFlareProtectedException): Boolean = suspendCoroutine { cont ->
|
||||||
continuations[CloudFlareActivity.TAG] = cont
|
continuations[CloudFlareActivity.TAG] = cont
|
||||||
cloudflareContract.launch(url to headers)
|
cloudflareContract.launch(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
||||||
@@ -81,13 +98,37 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openInBrowser(url: String) {
|
private fun openInBrowser(url: String) {
|
||||||
val context = activity ?: fragment?.activity ?: return
|
context?.run {
|
||||||
context.startActivity(BrowserActivity.newIntent(context, url, null, null))
|
startActivity(BrowserActivity.newIntent(this, url, null, null))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAlternatives(manga: Manga) {
|
private fun openAlternatives(manga: Manga) {
|
||||||
val context = activity ?: fragment?.activity ?: return
|
context?.run {
|
||||||
context.startActivity(AlternativesActivity.newIntent(context, manga))
|
startActivity(AlternativesActivity.newIntent(this, manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSslErrorDialog() {
|
||||||
|
val ctx = context ?: return
|
||||||
|
val settings = getAppSettings(ctx)
|
||||||
|
if (settings.isSSLBypassEnabled) {
|
||||||
|
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(R.string.ignore_ssl_errors)
|
||||||
|
.setMessage(R.string.ignore_ssl_errors_summary)
|
||||||
|
.setPositiveButton(R.string.apply) { _, _ ->
|
||||||
|
settings.isSSLBypassEnabled = true
|
||||||
|
Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_SHORT).show()
|
||||||
|
ctx.findActivity()?.finishAffinity()
|
||||||
|
}.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppSettings(context: Context): AppSettings {
|
||||||
|
return EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context).settings
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
||||||
@@ -100,6 +141,9 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
is AuthRequiredException -> R.string.sign_in
|
is AuthRequiredException -> R.string.sign_in
|
||||||
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
|
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
|
||||||
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
||||||
|
is SSLException,
|
||||||
|
is CertPathValidatorException -> R.string.fix
|
||||||
|
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.util.formatSimple
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.text.DecimalFormatSymbols
|
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@JvmName("mangaIds")
|
@JvmName("mangaIds")
|
||||||
@@ -119,17 +118,10 @@ val Manga.appUrl: Uri
|
|||||||
.appendQueryParameter("url", url)
|
.appendQueryParameter("url", url)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val chaptersNumberFormat = DecimalFormat("#.#").also { f ->
|
fun MangaChapter.formatNumber(): String? = if (number > 0f) {
|
||||||
f.decimalFormatSymbols = DecimalFormatSymbols.getInstance().also {
|
number.formatSimple()
|
||||||
it.decimalSeparator = '.'
|
} else {
|
||||||
}
|
null
|
||||||
}
|
|
||||||
|
|
||||||
fun MangaChapter.formatNumber(): String? {
|
|
||||||
if (number <= 0f) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return chaptersNumberFormat.format(number.toDouble())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Manga.chaptersCount(): Int {
|
fun Manga.chaptersCount(): Int {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|||||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
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.toTitleCase
|
|
||||||
import java.util.Locale
|
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
fun MangaSource(name: String): MangaSource {
|
fun MangaSource(name: String): MangaSource {
|
||||||
@@ -39,7 +37,7 @@ val ContentType.titleResId
|
|||||||
|
|
||||||
fun MangaSource.getSummary(context: Context): String {
|
fun MangaSource.getSummary(context: Context): String {
|
||||||
val type = context.getString(contentType.titleResId)
|
val type = context.getString(contentType.titleResId)
|
||||||
val locale = locale?.toLocale().getDisplayName(context)
|
val locale = locale.toLocale().getDisplayName(context)
|
||||||
return context.getString(R.string.source_summary_pattern, type, locale)
|
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import android.util.Log
|
|||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Interceptor.Chain
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
@@ -13,6 +15,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
|||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mergeWith
|
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import java.net.IDN
|
import java.net.IDN
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -23,7 +26,7 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
private val mangaLoaderContextLazy: Lazy<MangaLoaderContextImpl>,
|
private val mangaLoaderContextLazy: Lazy<MangaLoaderContextImpl>,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val source = request.tag(MangaSource::class.java)
|
val source = request.tag(MangaSource::class.java)
|
||||||
val repository = if (source != null) {
|
val repository = if (source != null) {
|
||||||
@@ -46,7 +49,7 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
headersBuilder.trySet(CommonHeaders.REFERER, "https://$idn/")
|
headersBuilder.trySet(CommonHeaders.REFERER, "https://$idn/")
|
||||||
}
|
}
|
||||||
val newRequest = request.newBuilder().headers(headersBuilder.build()).build()
|
val newRequest = request.newBuilder().headers(headersBuilder.build()).build()
|
||||||
return repository?.intercept(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
|
return repository?.interceptSafe(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Headers.Builder.trySet(name: String, value: String) = try {
|
private fun Headers.Builder.trySet(name: String, value: String) = try {
|
||||||
@@ -55,10 +58,21 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Interceptor.interceptSafe(chain: Chain): Response = runCatchingCancellable {
|
||||||
|
intercept(chain)
|
||||||
|
}.getOrElse { e ->
|
||||||
|
if (e is IOException) {
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
// only IOException can be safely thrown from an Interceptor
|
||||||
|
throw IOException("Error in interceptor: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class ProxyChain(
|
private class ProxyChain(
|
||||||
private val delegate: Interceptor.Chain,
|
private val delegate: Chain,
|
||||||
private val request: Request,
|
private val request: Request,
|
||||||
) : Interceptor.Chain by delegate {
|
) : Chain by delegate {
|
||||||
|
|
||||||
override fun request(): Request = request
|
override fun request(): Request = request
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ class DoHManager(
|
|||||||
tryGetByIp("2a10:50c0::2:ff"),
|
tryGetByIp("2a10:50c0::2:ff"),
|
||||||
),
|
),
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
|
DoHProvider.ZERO_MS -> DnsOverHttps.Builder().client(bootstrapClient)
|
||||||
|
.url("https://2ca4h4crra.cloudflare-gateway.com/dns-query".toHttpUrl())
|
||||||
|
.resolvePublicAddresses(true)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryGetByIp(ip: String): InetAddress? = try {
|
private fun tryGetByIp(ip: String): InetAddress? = try {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.network
|
|||||||
|
|
||||||
enum class DoHProvider {
|
enum class DoHProvider {
|
||||||
|
|
||||||
NONE, GOOGLE, CLOUDFLARE, ADGUARD
|
NONE, GOOGLE, CLOUDFLARE, ADGUARD, ZERO_MS
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import coil.intercept.Interceptor
|
|
||||||
import coil.request.ErrorResult
|
|
||||||
import coil.request.ImageResult
|
|
||||||
import coil.request.SuccessResult
|
|
||||||
import coil.size.Dimension
|
|
||||||
import coil.size.isOriginal
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
import java.util.Collections
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class ImageProxyInterceptor @Inject constructor(
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) : Interceptor {
|
|
||||||
|
|
||||||
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
|
||||||
|
|
||||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
|
||||||
val request = chain.request
|
|
||||||
if (!settings.isImagesProxyEnabled) {
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
val url: HttpUrl? = when (val data = request.data) {
|
|
||||||
is HttpUrl -> data
|
|
||||||
is String -> data.toHttpUrlOrNull()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
val newUrl = HttpUrl.Builder()
|
|
||||||
.scheme("https")
|
|
||||||
.host("wsrv.nl")
|
|
||||||
.addQueryParameter("url", url.toString())
|
|
||||||
.addQueryParameter("we", null)
|
|
||||||
val size = request.sizeResolver.size()
|
|
||||||
if (!size.isOriginal) {
|
|
||||||
newUrl.addQueryParameter("crop", "cover")
|
|
||||||
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
|
||||||
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val newRequest = request.newBuilder()
|
|
||||||
.data(newUrl.build())
|
|
||||||
.build()
|
|
||||||
val result = chain.proceed(newRequest)
|
|
||||||
return if (result is SuccessResult) {
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
logDebug((result as? ErrorResult)?.throwable)
|
|
||||||
chain.proceed(request).also {
|
|
||||||
if (it is SuccessResult) {
|
|
||||||
blacklist.add(url.host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
|
||||||
if (!settings.isImagesProxyEnabled) {
|
|
||||||
return okHttp.newCall(request).await()
|
|
||||||
}
|
|
||||||
val sourceUrl = request.url
|
|
||||||
val targetUrl = HttpUrl.Builder()
|
|
||||||
.scheme("https")
|
|
||||||
.host("wsrv.nl")
|
|
||||||
.addQueryParameter("url", sourceUrl.toString())
|
|
||||||
.addQueryParameter("we", null)
|
|
||||||
val newRequest = request.newBuilder()
|
|
||||||
.url(targetUrl.build())
|
|
||||||
.build()
|
|
||||||
return runCatchingCancellable {
|
|
||||||
okHttp.doCall(newRequest)
|
|
||||||
}.recover {
|
|
||||||
logDebug(it)
|
|
||||||
okHttp.doCall(request).also {
|
|
||||||
blacklist.add(sourceUrl.host)
|
|
||||||
}
|
|
||||||
}.getOrThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
|
||||||
return newCall(request).await().ensureSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logDebug(e: Throwable?) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.w("ImageProxy", e.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,13 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Provider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -27,6 +31,9 @@ interface NetworkModule {
|
|||||||
@Binds
|
@Binds
|
||||||
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindImageProxyInterceptor(impl: RealImageProxyInterceptor): ImageProxyInterceptor
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -50,10 +57,12 @@ interface NetworkModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
@BaseHttpClient
|
@BaseHttpClient
|
||||||
fun provideBaseHttpClient(
|
fun provideBaseHttpClient(
|
||||||
|
@ApplicationContext contextProvider: Provider<Context>,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
cookieJar: CookieJar,
|
cookieJar: CookieJar,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
): OkHttpClient = OkHttpClient.Builder().apply {
|
): OkHttpClient = OkHttpClient.Builder().apply {
|
||||||
|
assertNotInMainThread()
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
connectTimeout(20, TimeUnit.SECONDS)
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
readTimeout(60, TimeUnit.SECONDS)
|
||||||
writeTimeout(20, TimeUnit.SECONDS)
|
writeTimeout(20, TimeUnit.SECONDS)
|
||||||
@@ -62,7 +71,9 @@ interface NetworkModule {
|
|||||||
proxyAuthenticator(ProxyAuthenticator(settings))
|
proxyAuthenticator(ProxyAuthenticator(settings))
|
||||||
dns(DoHManager(cache, settings))
|
dns(DoHManager(cache, settings))
|
||||||
if (settings.isSSLBypassEnabled) {
|
if (settings.isSSLBypassEnabled) {
|
||||||
bypassSSLErrors()
|
disableCertificateVerification()
|
||||||
|
} else {
|
||||||
|
installExtraCertsificates(contextProvider.get())
|
||||||
}
|
}
|
||||||
cache(cache)
|
cache(cache)
|
||||||
addInterceptor(GZipInterceptor())
|
addInterceptor(GZipInterceptor())
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import javax.net.ssl.SSLContext
|
|
||||||
import javax.net.ssl.SSLSocketFactory
|
|
||||||
import javax.net.ssl.X509TrustManager
|
|
||||||
|
|
||||||
@SuppressLint("CustomX509TrustManager")
|
|
||||||
fun OkHttpClient.Builder.bypassSSLErrors() = also { builder ->
|
|
||||||
runCatching {
|
|
||||||
val trustAllCerts = object : X509TrustManager {
|
|
||||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
||||||
|
|
||||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
||||||
|
|
||||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
|
||||||
}
|
|
||||||
val sslContext = SSLContext.getInstance("SSL")
|
|
||||||
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
|
|
||||||
val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
|
|
||||||
builder.sslSocketFactory(sslSocketFactory, trustAllCerts)
|
|
||||||
builder.hostnameVerifier { _, _ -> true }
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.util.Log
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.tls.HandshakeCertificates
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
@SuppressLint("CustomX509TrustManager")
|
||||||
|
fun OkHttpClient.Builder.disableCertificateVerification() = also { builder ->
|
||||||
|
runCatching {
|
||||||
|
val trustAllCerts = object : X509TrustManager {
|
||||||
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||||
|
|
||||||
|
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||||
|
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||||
|
}
|
||||||
|
val sslContext = SSLContext.getInstance("SSL")
|
||||||
|
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
|
||||||
|
val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
|
||||||
|
builder.sslSocketFactory(sslSocketFactory, trustAllCerts)
|
||||||
|
builder.hostnameVerifier { _, _ -> true }
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTraceDebug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.installExtraCertsificates(context: Context) = also { builder ->
|
||||||
|
val certificatesBuilder = HandshakeCertificates.Builder()
|
||||||
|
.addPlatformTrustedCertificates()
|
||||||
|
val assets = context.assets.list("").orEmpty()
|
||||||
|
for (path in assets) {
|
||||||
|
if (path.endsWith(".pem")) {
|
||||||
|
val cert = loadCert(context, path) ?: continue
|
||||||
|
certificatesBuilder.addTrustedCertificate(cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val certificates = certificatesBuilder.build()
|
||||||
|
builder.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCert(context: Context, path: String): X509Certificate? = runCatching {
|
||||||
|
val cf = CertificateFactory.getInstance("X.509")
|
||||||
|
context.assets.open(path, AssetManager.ACCESS_STREAMING).use {
|
||||||
|
cf.generateCertificate(it)
|
||||||
|
} as X509Certificate
|
||||||
|
}.onFailure { e ->
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
}.onSuccess {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.i("ExtraCerts", "Loaded cert $path")
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.network.HttpException
|
||||||
|
import coil.request.ErrorResult
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import coil.request.SuccessResult
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.HttpStatusException
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
abstract class BaseImageProxyInterceptor : ImageProxyInterceptor {
|
||||||
|
|
||||||
|
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||||
|
|
||||||
|
final override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
val request = chain.request
|
||||||
|
val url: HttpUrl? = when (val data = request.data) {
|
||||||
|
is HttpUrl -> data
|
||||||
|
is String -> data.toHttpUrlOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val newRequest = onInterceptImageRequest(request, url)
|
||||||
|
return when (val result = chain.proceed(newRequest)) {
|
||||||
|
is SuccessResult -> result
|
||||||
|
is ErrorResult -> {
|
||||||
|
logDebug(result.throwable, newRequest.data)
|
||||||
|
chain.proceed(request).also {
|
||||||
|
if (it is SuccessResult && result.throwable.isBlockedByServer()) {
|
||||||
|
blacklist.add(url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
val newRequest = onInterceptPageRequest(request)
|
||||||
|
return runCatchingCancellable {
|
||||||
|
okHttp.doCall(newRequest)
|
||||||
|
}.recover { error ->
|
||||||
|
logDebug(error, newRequest.url)
|
||||||
|
okHttp.doCall(request).also {
|
||||||
|
if (error.isBlockedByServer()) {
|
||||||
|
blacklist.add(request.url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest
|
||||||
|
|
||||||
|
protected abstract suspend fun onInterceptPageRequest(request: Request): Request
|
||||||
|
|
||||||
|
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||||
|
return newCall(request).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logDebug(e: Throwable, url: Any) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w("ImageProxy", "${e.message}: $url", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Throwable.isBlockedByServer(): Boolean {
|
||||||
|
return this is CloudFlareBlockedException
|
||||||
|
|| (this is HttpException && response.code == HttpURLConnection.HTTP_FORBIDDEN)
|
||||||
|
|| (this is HttpStatusException && statusCode == HttpURLConnection.HTTP_FORBIDDEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
interface ImageProxyInterceptor : Interceptor {
|
||||||
|
|
||||||
|
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class RealImageProxyInterceptor @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : ImageProxyInterceptor {
|
||||||
|
|
||||||
|
private val delegate = settings.observeAsStateFlow(
|
||||||
|
scope = processLifecycleScope + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_IMAGES_PROXY,
|
||||||
|
valueProducer = { createDelegate() },
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
return delegate.value?.intercept(chain) ?: chain.proceed(chain.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
return delegate.value?.interceptPageRequest(request, okHttp) ?: okHttp.newCall(request).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDelegate(): ImageProxyInterceptor? = when (val proxy = settings.imagesProxy) {
|
||||||
|
-1 -> null
|
||||||
|
0 -> WsrvNlProxyInterceptor()
|
||||||
|
1 -> ZeroMsProxyInterceptor()
|
||||||
|
else -> error("Unsupported images proxy $proxy")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.size.Dimension
|
||||||
|
import coil.size.isOriginal
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
class WsrvNlProxyInterceptor : BaseImageProxyInterceptor() {
|
||||||
|
|
||||||
|
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
|
||||||
|
val newUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", url.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val size = request.sizeResolver.size()
|
||||||
|
if (!size.isOriginal) {
|
||||||
|
newUrl.addQueryParameter("crop", "cover")
|
||||||
|
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||||
|
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.newBuilder()
|
||||||
|
.data(newUrl.build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onInterceptPageRequest(request: Request): Request {
|
||||||
|
val sourceUrl = request.url
|
||||||
|
val targetUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", sourceUrl.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
return request.newBuilder()
|
||||||
|
.url(targetUrl.build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.imageproxy
|
||||||
|
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
class ZeroMsProxyInterceptor : BaseImageProxyInterceptor() {
|
||||||
|
|
||||||
|
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
|
||||||
|
if (url.host == "x.0ms.dev" || url.host == "0ms.dev") {
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
val newUrl = ("https://x.0ms.dev/q70/$url").toHttpUrl()
|
||||||
|
return request.newBuilder()
|
||||||
|
.data(newUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onInterceptPageRequest(request: Request): Request {
|
||||||
|
val newUrl = ("https://x.0ms.dev/q70/${request.url}").toHttpUrl()
|
||||||
|
return request.newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
@@ -31,6 +32,7 @@ import org.koitharu.kotatsu.core.util.ext.source
|
|||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
@@ -90,6 +92,14 @@ class AppShortcutManager @Inject constructor(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMangaShortcuts(): Set<Long> {
|
||||||
|
val shortcuts = ShortcutManagerCompat.getShortcuts(
|
||||||
|
context,
|
||||||
|
ShortcutManagerCompat.FLAG_MATCH_CACHED or ShortcutManagerCompat.FLAG_MATCH_PINNED or ShortcutManagerCompat.FLAG_MATCH_DYNAMIC,
|
||||||
|
)
|
||||||
|
return shortcuts.mapNotNullToSet { it.id.toLongOrNull() }
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
suspend fun await(): Boolean {
|
suspend fun await(): Boolean {
|
||||||
return shortcutsUpdateJob?.join() != null
|
return shortcutsUpdateJob?.join() != null
|
||||||
@@ -150,7 +160,7 @@ class AppShortcutManager @Inject constructor(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat {
|
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat = withContext(Dispatchers.Default) {
|
||||||
val icon = runCatchingCancellable {
|
val icon = runCatchingCancellable {
|
||||||
coil.execute(
|
coil.execute(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
@@ -163,7 +173,7 @@ class AppShortcutManager @Inject constructor(
|
|||||||
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
||||||
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
|
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
|
||||||
)
|
)
|
||||||
return ShortcutInfoCompat.Builder(context, source.name)
|
ShortcutInfoCompat.Builder(context, source.name)
|
||||||
.setShortLabel(source.title)
|
.setShortLabel(source.title)
|
||||||
.setLongLabel(source.title)
|
.setLongLabel(source.title)
|
||||||
.setIcon(icon)
|
.setIcon(icon)
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import android.net.ConnectivityManager.NetworkCallback
|
|||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.MediatorStateFlow
|
import org.koitharu.kotatsu.core.util.MediatorStateFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.isOnline
|
|
||||||
|
|
||||||
class NetworkState(
|
class NetworkState(
|
||||||
private val connectivityManager: ConnectivityManager,
|
private val connectivityManager: ConnectivityManager,
|
||||||
) : MediatorStateFlow<Boolean>(connectivityManager.isOnline()) {
|
private val settings: AppSettings,
|
||||||
|
) : MediatorStateFlow<Boolean>(connectivityManager.isOnline(settings)) {
|
||||||
|
|
||||||
private val callback = NetworkCallbackImpl()
|
private val callback = NetworkCallbackImpl()
|
||||||
|
|
||||||
@@ -19,7 +21,10 @@ class NetworkState(
|
|||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
invalidate()
|
invalidate()
|
||||||
val request = NetworkRequest.Builder()
|
val request = NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
.build()
|
.build()
|
||||||
connectivityManager.registerNetworkCallback(request, callback)
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
}
|
}
|
||||||
@@ -37,7 +42,7 @@ class NetworkState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun invalidate() {
|
private fun invalidate() {
|
||||||
publishValue(connectivityManager.isOnline())
|
publishValue(connectivityManager.isOnline(settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NetworkCallbackImpl : NetworkCallback() {
|
private inner class NetworkCallbackImpl : NetworkCallback() {
|
||||||
@@ -48,4 +53,27 @@ class NetworkState(
|
|||||||
|
|
||||||
override fun onUnavailable() = invalidate()
|
override fun onUnavailable() = invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
fun ConnectivityManager.isOnline(settings: AppSettings): Boolean {
|
||||||
|
if (settings.isOfflineCheckDisabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
activeNetwork?.let { isOnline(it) } ?: false
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activeNetworkInfo?.isConnected == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ConnectivityManager.isOnline(network: Network): Boolean {
|
||||||
|
val capabilities = getNetworkCapabilities(network) ?: return false
|
||||||
|
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||||
|
import org.koitharu.kotatsu.parsers.bitmap.Rect
|
||||||
|
import java.io.OutputStream
|
||||||
|
import android.graphics.Bitmap as AndroidBitmap
|
||||||
|
import android.graphics.Rect as AndroidRect
|
||||||
|
|
||||||
|
class BitmapWrapper private constructor(
|
||||||
|
private val androidBitmap: AndroidBitmap,
|
||||||
|
) : Bitmap {
|
||||||
|
|
||||||
|
private val canvas by lazy { Canvas(androidBitmap) } // is not always used, so initialized lazily
|
||||||
|
|
||||||
|
override val height: Int
|
||||||
|
get() = androidBitmap.height
|
||||||
|
|
||||||
|
override val width: Int
|
||||||
|
get() = androidBitmap.width
|
||||||
|
|
||||||
|
override fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) {
|
||||||
|
val androidSourceBitmap = (sourceBitmap as BitmapWrapper).androidBitmap
|
||||||
|
canvas.drawBitmap(androidSourceBitmap, src.toAndroidRect(), dst.toAndroidRect(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compressTo(output: OutputStream) {
|
||||||
|
androidBitmap.compress(AndroidBitmap.CompressFormat.PNG, 100, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(width: Int, height: Int): Bitmap = BitmapWrapper(
|
||||||
|
AndroidBitmap.createBitmap(width, height, AndroidBitmap.Config.ARGB_8888),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun create(bitmap: AndroidBitmap): Bitmap = BitmapWrapper(
|
||||||
|
if (bitmap.isMutable) bitmap else bitmap.copy(AndroidBitmap.Config.ARGB_8888, true),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Rect.toAndroidRect() = AndroidRect(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.parser
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
@@ -10,23 +11,29 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||||
|
import okio.Buffer
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.requireBody
|
||||||
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
||||||
import org.koitharu.kotatsu.core.util.ext.toList
|
import org.koitharu.kotatsu.core.util.ext.toList
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@@ -38,15 +45,10 @@ class MangaLoaderContextImpl @Inject constructor(
|
|||||||
) : MangaLoaderContext() {
|
) : MangaLoaderContext() {
|
||||||
|
|
||||||
private var webViewCached: WeakReference<WebView>? = null
|
private var webViewCached: WeakReference<WebView>? = null
|
||||||
|
private val webViewUserAgent by lazy { obtainWebViewUserAgent() }
|
||||||
private val userAgentLazy = SuspendLazy {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
obtainWebView().settings.userAgentString
|
|
||||||
}.sanitizeHeaderValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main) {
|
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main.immediate) {
|
||||||
val webView = obtainWebView()
|
val webView = obtainWebView()
|
||||||
suspendCoroutine { cont ->
|
suspendCoroutine { cont ->
|
||||||
webView.evaluateJavascript(script) { result ->
|
webView.evaluateJavascript(script) { result ->
|
||||||
@@ -55,13 +57,7 @@ class MangaLoaderContextImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDefaultUserAgent(): String = runCatching {
|
override fun getDefaultUserAgent(): String = webViewUserAgent
|
||||||
runBlocking {
|
|
||||||
userAgentLazy.get()
|
|
||||||
}
|
|
||||||
}.onFailure { e ->
|
|
||||||
e.printStackTraceDebug()
|
|
||||||
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
|
|
||||||
|
|
||||||
override fun getConfig(source: MangaSource): MangaSourceConfig {
|
override fun getConfig(source: MangaSource): MangaSourceConfig {
|
||||||
return SourceSettings(androidContext, source)
|
return SourceSettings(androidContext, source)
|
||||||
@@ -79,6 +75,27 @@ class MangaLoaderContextImpl @Inject constructor(
|
|||||||
return LocaleListCompat.getAdjustedDefault().toList()
|
return LocaleListCompat.getAdjustedDefault().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
|
||||||
|
val image = response.requireBody().byteStream()
|
||||||
|
|
||||||
|
val opts = BitmapFactory.Options()
|
||||||
|
opts.inMutable = true
|
||||||
|
val bitmap = BitmapFactory.decodeStream(image, null, opts) ?: error("Cannot decode bitmap")
|
||||||
|
val result = redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper
|
||||||
|
|
||||||
|
val body = Buffer().also {
|
||||||
|
result.compressTo(it.outputStream())
|
||||||
|
}.asResponseBody("image/jpeg".toMediaType())
|
||||||
|
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(body)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createBitmap(width: Int, height: Int): Bitmap {
|
||||||
|
return BitmapWrapper.create(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
private fun obtainWebView(): WebView {
|
private fun obtainWebView(): WebView {
|
||||||
return webViewCached?.get() ?: WebView(androidContext).also {
|
return webViewCached?.get() ?: WebView(androidContext).also {
|
||||||
@@ -86,4 +103,22 @@ class MangaLoaderContextImpl @Inject constructor(
|
|||||||
webViewCached = WeakReference(it)
|
webViewCached = WeakReference(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun obtainWebViewUserAgent(): String {
|
||||||
|
val mainDispatcher = Dispatchers.Main.immediate
|
||||||
|
return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
||||||
|
obtainWebViewUserAgentImpl()
|
||||||
|
} else {
|
||||||
|
runBlocking(mainDispatcher) {
|
||||||
|
obtainWebViewUserAgentImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
private fun obtainWebViewUserAgentImpl() = runCatching {
|
||||||
|
obtainWebView().settings.userAgentString.sanitizeHeaderValue()
|
||||||
|
}.onFailure { e ->
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
@@ -57,7 +57,7 @@ interface MangaRepository {
|
|||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val loaderContext: MangaLoaderContext,
|
private val loaderContext: MangaLoaderContext,
|
||||||
private val contentCache: ContentCache,
|
private val contentCache: MemoryContentCache,
|
||||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ import okhttp3.Headers
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.MultiMutex
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
import org.koitharu.kotatsu.parsers.model.Favicons
|
import org.koitharu.kotatsu.parsers.model.Favicons
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -38,10 +38,14 @@ import java.util.Locale
|
|||||||
|
|
||||||
class RemoteMangaRepository(
|
class RemoteMangaRepository(
|
||||||
private val parser: MangaParser,
|
private val parser: MangaParser,
|
||||||
private val cache: ContentCache,
|
private val cache: MemoryContentCache,
|
||||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
) : MangaRepository, Interceptor {
|
) : MangaRepository, Interceptor {
|
||||||
|
|
||||||
|
private val detailsMutex = MultiMutex<Long>()
|
||||||
|
private val relatedMangaMutex = MultiMutex<Long>()
|
||||||
|
private val pagesMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
override val source: MangaSource
|
override val source: MangaSource
|
||||||
get() = parser.source
|
get() = parser.source
|
||||||
|
|
||||||
@@ -97,7 +101,7 @@ class RemoteMangaRepository(
|
|||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
|
||||||
cache.getPages(source, chapter.url)?.let { return it }
|
cache.getPages(source, chapter.url)?.let { return it }
|
||||||
val pages = asyncSafe {
|
val pages = asyncSafe {
|
||||||
mirrorSwitchInterceptor.withMirrorSwitching {
|
mirrorSwitchInterceptor.withMirrorSwitching {
|
||||||
@@ -105,8 +109,8 @@ class RemoteMangaRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.putPages(source, chapter.url, pages)
|
cache.putPages(source, chapter.url, pages)
|
||||||
return pages.await()
|
pages
|
||||||
}
|
}.await()
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
|
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
|
||||||
parser.getPageUrl(page)
|
parser.getPageUrl(page)
|
||||||
@@ -124,16 +128,16 @@ class RemoteMangaRepository(
|
|||||||
parser.getFavicons()
|
parser.getFavicons()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRelated(seed: Manga): List<Manga> {
|
override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
|
||||||
cache.getRelatedManga(source, seed.url)?.let { return it }
|
cache.getRelatedManga(source, seed.url)?.let { return it }
|
||||||
val related = asyncSafe {
|
val related = asyncSafe {
|
||||||
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
||||||
}
|
}
|
||||||
cache.putRelatedManga(source, seed.url, related)
|
cache.putRelatedManga(source, seed.url, related)
|
||||||
return related.await()
|
related
|
||||||
}
|
}.await()
|
||||||
|
|
||||||
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga {
|
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
|
||||||
if (cachePolicy.readEnabled) {
|
if (cachePolicy.readEnabled) {
|
||||||
cache.getDetails(source, manga.url)?.let { return it }
|
cache.getDetails(source, manga.url)?.let { return it }
|
||||||
}
|
}
|
||||||
@@ -145,8 +149,8 @@ class RemoteMangaRepository(
|
|||||||
if (cachePolicy.writeEnabled) {
|
if (cachePolicy.writeEnabled) {
|
||||||
cache.putDetails(source, manga.url, details)
|
cache.putDetails(source, manga.url, details)
|
||||||
}
|
}
|
||||||
return details.await()
|
details
|
||||||
}
|
}.await()
|
||||||
|
|
||||||
suspend fun peekDetails(manga: Manga): Manga? {
|
suspend fun peekDetails(manga: Manga): Manga? {
|
||||||
return cache.getDetails(source, manga.url)
|
return cache.getDetails(source, manga.url)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.requireBody
|
||||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||||
@@ -150,10 +151,6 @@ class FaviconFetcher(
|
|||||||
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
|
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Response.requireBody(): ResponseBody {
|
|
||||||
return checkNotNull(body) { "response body == null" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Size.toCacheKey() = buildString {
|
private fun Size.toCacheKey() = buildString {
|
||||||
append(width.toString())
|
append(width.toString())
|
||||||
append('x')
|
append('x')
|
||||||
@@ -170,10 +167,11 @@ class FaviconFetcher(
|
|||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val okHttpClient: OkHttpClient,
|
okHttpClientLazy: Lazy<OkHttpClient>,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) : Fetcher.Factory<Uri> {
|
) : Fetcher.Factory<Uri> {
|
||||||
|
|
||||||
|
private val okHttpClient by okHttpClientLazy
|
||||||
private val diskCache = lazy {
|
private val diskCache = lazy {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
DiskCache.Builder()
|
DiskCache.Builder()
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
|
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
|
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
|
||||||
|
|
||||||
|
val isOfflineCheckDisabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_OFFLINE_DISABLED, false)
|
||||||
|
|
||||||
var isAllFavouritesVisible: Boolean
|
var isAllFavouritesVisible: Boolean
|
||||||
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
|
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
|
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
|
||||||
@@ -152,6 +155,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isTrackerNotificationsEnabled: Boolean
|
val isTrackerNotificationsEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
||||||
|
|
||||||
|
val isTrackerNsfwDisabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_TRACKER_NO_NSFW, false)
|
||||||
|
|
||||||
var notificationSound: Uri
|
var notificationSound: Uri
|
||||||
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
|
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
|
||||||
?: Settings.System.DEFAULT_NOTIFICATION_URI
|
?: Settings.System.DEFAULT_NOTIFICATION_URI
|
||||||
@@ -252,7 +258,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
|
|
||||||
val defaultDetailsTab: Int
|
val defaultDetailsTab: Int
|
||||||
get() = if (isPagesTabEnabled) {
|
get() = if (isPagesTabEnabled) {
|
||||||
val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: 0
|
val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: -1
|
||||||
if (raw == -1) {
|
if (raw == -1) {
|
||||||
lastDetailsTab
|
lastDetailsTab
|
||||||
} else {
|
} else {
|
||||||
@@ -281,20 +287,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
set(value) = prefs.edit { putEnumValue(KEY_SOURCES_ORDER, value) }
|
set(value) = prefs.edit { putEnumValue(KEY_SOURCES_ORDER, value) }
|
||||||
|
|
||||||
var isSourcesGridMode: Boolean
|
var isSourcesGridMode: Boolean
|
||||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
|
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||||
|
|
||||||
val isNewSourcesTipEnabled: Boolean
|
var sourcesVersion: Int
|
||||||
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
|
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
|
||||||
|
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
|
||||||
|
|
||||||
val isPagesNumbersEnabled: Boolean
|
val isPagesNumbersEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||||
|
|
||||||
val screenshotsPolicy: ScreenshotsPolicy
|
val screenshotsPolicy: ScreenshotsPolicy
|
||||||
get() = runCatching {
|
get() = prefs.getEnumValue(KEY_SCREENSHOTS_POLICY, ScreenshotsPolicy.ALLOW)
|
||||||
val key = prefs.getString(KEY_SCREENSHOTS_POLICY, null)?.uppercase(Locale.ROOT)
|
|
||||||
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
|
|
||||||
}.getOrDefault(ScreenshotsPolicy.ALLOW)
|
|
||||||
|
|
||||||
var userSpecifiedMangaDirectories: Set<File>
|
var userSpecifiedMangaDirectories: Set<File>
|
||||||
get() {
|
get() {
|
||||||
@@ -377,14 +381,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isImagesProxyEnabled: Boolean
|
val imagesProxy: Int
|
||||||
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
|
get() {
|
||||||
|
val raw = prefs.getString(KEY_IMAGES_PROXY, null)?.toIntOrNull()
|
||||||
|
return raw ?: if (prefs.getBoolean(KEY_IMAGES_PROXY_OLD, false)) 0 else -1
|
||||||
|
}
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
val isSSLBypassEnabled: Boolean
|
var isSSLBypassEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
|
get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
|
||||||
|
set(value) = prefs.edit { putBoolean(KEY_SSL_BYPASS, value) }
|
||||||
|
|
||||||
val proxyType: Proxy.Type
|
val proxyType: Proxy.Type
|
||||||
get() {
|
get() {
|
||||||
@@ -544,8 +552,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val PAGE_SWITCH_VOLUME_KEYS = "volume"
|
|
||||||
|
|
||||||
const val TRACK_HISTORY = "history"
|
const val TRACK_HISTORY = "history"
|
||||||
const val TRACK_FAVOURITES = "favourites"
|
const val TRACK_FAVOURITES = "favourites"
|
||||||
|
|
||||||
@@ -557,6 +563,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_COLOR_THEME = "color_theme"
|
const val KEY_COLOR_THEME = "color_theme"
|
||||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||||
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
||||||
|
const val KEY_OFFLINE_DISABLED = "no_offline"
|
||||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
||||||
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
|
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
|
||||||
const val KEY_COOKIES_CLEAR = "cookies_clear"
|
const val KEY_COOKIES_CLEAR = "cookies_clear"
|
||||||
@@ -581,6 +588,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_TRACK_CATEGORIES = "track_categories"
|
const val KEY_TRACK_CATEGORIES = "track_categories"
|
||||||
const val KEY_TRACK_WARNING = "track_warning"
|
const val KEY_TRACK_WARNING = "track_warning"
|
||||||
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
|
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
|
||||||
|
const val KEY_TRACKER_NO_NSFW = "tracker_no_nsfw"
|
||||||
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
|
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
|
||||||
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
|
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
|
||||||
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
|
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
|
||||||
@@ -593,7 +601,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
|
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
|
||||||
const val KEY_PROTECT_APP = "protect_app"
|
const val KEY_PROTECT_APP = "protect_app"
|
||||||
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
|
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
|
||||||
const val KEY_APP_VERSION = "app_version"
|
|
||||||
const val KEY_ZOOM_MODE = "zoom_mode"
|
const val KEY_ZOOM_MODE = "zoom_mode"
|
||||||
const val KEY_BACKUP = "backup"
|
const val KEY_BACKUP = "backup"
|
||||||
const val KEY_RESTORE = "restore"
|
const val KEY_RESTORE = "restore"
|
||||||
@@ -643,9 +650,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||||
const val KEY_APP_LOCALE = "app_locale"
|
const val KEY_APP_LOCALE = "app_locale"
|
||||||
const val KEY_LOGGING_ENABLED = "logging"
|
const val KEY_LOGGING_ENABLED = "logging"
|
||||||
const val KEY_LOGS_SHARE = "logs_share"
|
|
||||||
const val KEY_SOURCES_GRID = "sources_grid"
|
const val KEY_SOURCES_GRID = "sources_grid"
|
||||||
const val KEY_SOURCES_NEW = "sources_new"
|
|
||||||
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
||||||
const val KEY_TIPS_CLOSED = "tips_closed"
|
const val KEY_TIPS_CLOSED = "tips_closed"
|
||||||
const val KEY_SSL_BYPASS = "ssl_bypass"
|
const val KEY_SSL_BYPASS = "ssl_bypass"
|
||||||
@@ -658,7 +663,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PROXY_AUTH = "proxy_auth"
|
const val KEY_PROXY_AUTH = "proxy_auth"
|
||||||
const val KEY_PROXY_LOGIN = "proxy_login"
|
const val KEY_PROXY_LOGIN = "proxy_login"
|
||||||
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_2"
|
||||||
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_DISABLE_NSFW = "no_nsfw"
|
||||||
const val KEY_RELATED_MANGA = "related_manga"
|
const val KEY_RELATED_MANGA = "related_manga"
|
||||||
@@ -672,7 +677,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_CF_CONTRAST = "cf_contrast"
|
const val KEY_CF_CONTRAST = "cf_contrast"
|
||||||
const val KEY_CF_INVERTED = "cf_inverted"
|
const val KEY_CF_INVERTED = "cf_inverted"
|
||||||
const val KEY_CF_GRAYSCALE = "cf_grayscale"
|
const val KEY_CF_GRAYSCALE = "cf_grayscale"
|
||||||
const val KEY_IGNORE_DOZE = "ignore_dose"
|
|
||||||
const val KEY_PAGES_TAB = "pages_tab"
|
const val KEY_PAGES_TAB = "pages_tab"
|
||||||
const val KEY_DETAILS_TAB = "details_tab"
|
const val KEY_DETAILS_TAB = "details_tab"
|
||||||
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
|
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
|
||||||
@@ -680,9 +684,19 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PAGES_SAVE_DIR = "pages_dir"
|
const val KEY_PAGES_SAVE_DIR = "pages_dir"
|
||||||
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
|
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
|
||||||
const val KEY_STATS_ENABLED = "stats_on"
|
const val KEY_STATS_ENABLED = "stats_on"
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
|
||||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
|
||||||
const val KEY_FEED_HEADER = "feed_header"
|
const val KEY_FEED_HEADER = "feed_header"
|
||||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||||
|
const val KEY_SOURCES_VERSION = "sources_version"
|
||||||
|
|
||||||
|
// keys for non-persistent preferences
|
||||||
|
const val KEY_APP_VERSION = "app_version"
|
||||||
|
const val KEY_IGNORE_DOZE = "ignore_dose"
|
||||||
|
const val KEY_TRACKER_DEBUG = "tracker_debug"
|
||||||
|
const val KEY_LOGS_SHARE = "logs_share"
|
||||||
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
|
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||||
|
|
||||||
|
// old keys are for migration only
|
||||||
|
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.prefs
|
|||||||
enum class ScreenshotsPolicy {
|
enum class ScreenshotsPolicy {
|
||||||
|
|
||||||
// Do not rename this
|
// Do not rename this
|
||||||
ALLOW, BLOCK_NSFW, BLOCK_ALL;
|
ALLOW, BLOCK_NSFW, BLOCK_INCOGNITO, BLOCK_ALL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.prefs
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import okhttp3.internal.isSensitiveHeader
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getEnumValue
|
import org.koitharu.kotatsu.core.util.ext.getEnumValue
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
||||||
@@ -12,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
|
||||||
|
|
||||||
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
|
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
|
||||||
|
|
||||||
@@ -31,7 +31,11 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
|||||||
.ifNullOrEmpty { key.defaultValue }
|
.ifNullOrEmpty { key.defaultValue }
|
||||||
.sanitizeHeaderValue()
|
.sanitizeHeaderValue()
|
||||||
|
|
||||||
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
|
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue)
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { DomainValidator.isValidDomain(it) }
|
||||||
|
?: key.defaultValue
|
||||||
|
|
||||||
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
|
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
} as T
|
} as T
|
||||||
|
|||||||
@@ -7,34 +7,33 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.ActionBarContextView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
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.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||||
import org.koitharu.kotatsu.core.ui.util.BaseActivityEntryPoint
|
|
||||||
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.isWebViewUnavailable
|
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||||
|
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class BaseActivity<B : ViewBinding> :
|
abstract class BaseActivity<B : ViewBinding> :
|
||||||
AppCompatActivity(),
|
AppCompatActivity(),
|
||||||
|
ScreenshotPolicyHelper.ContentContainer,
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
WindowInsetsDelegate.WindowInsetsListener {
|
||||||
|
|
||||||
private var isAmoledTheme = false
|
private var isAmoledTheme = false
|
||||||
@@ -98,7 +97,20 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
dispatchNavigateUp()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
// TODO fix behavior on Android 14
|
||||||
|
dispatchNavigateUp()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val fm = supportFragmentManager
|
||||||
|
if (fm.isStateSaved) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (fm.backStackEntryCount > 0) {
|
||||||
|
fm.popBackStack()
|
||||||
|
} else {
|
||||||
|
dispatchNavigateUp()
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,32 +135,13 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
super.onSupportActionModeStarted(mode)
|
super.onSupportActionModeStarted(mode)
|
||||||
actionModeDelegate.onSupportActionModeStarted(mode)
|
actionModeDelegate.onSupportActionModeStarted(mode, window)
|
||||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
ColorUtils.compositeColors(
|
|
||||||
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
|
|
||||||
getThemeColor(com.google.android.material.R.attr.colorSurface),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ContextCompat.getColor(this, R.color.kotatsu_background)
|
|
||||||
}
|
|
||||||
defaultStatusBarColor = window.statusBarColor
|
|
||||||
window.statusBarColor = actionModeColor
|
|
||||||
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
|
|
||||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
|
||||||
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
|
|
||||||
setBackgroundColor(actionModeColor)
|
|
||||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = insets.top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||||
super.onSupportActionModeFinished(mode)
|
super.onSupportActionModeFinished(mode)
|
||||||
actionModeDelegate.onSupportActionModeFinished(mode)
|
actionModeDelegate.onSupportActionModeFinished(mode, window)
|
||||||
window.statusBarColor = defaultStatusBarColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun dispatchNavigateUp() {
|
protected open fun dispatchNavigateUp() {
|
||||||
@@ -162,6 +155,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
|
||||||
|
|
||||||
private fun putDataToExtras(intent: Intent?) {
|
private fun putDataToExtras(intent: Intent?) {
|
||||||
intent?.putExtra(EXTRA_DATA, intent.data)
|
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||||
}
|
}
|
||||||
@@ -181,6 +176,14 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun hasViewBinding() = ::viewBinding.isInitialized
|
||||||
|
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface BaseActivityEntryPoint {
|
||||||
|
val settings: AppSettings
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val EXTRA_DATA = "data"
|
const val EXTRA_DATA = "data"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
|
|||||||
with(window) {
|
with(window) {
|
||||||
systemUiController = SystemUiController(this)
|
systemUiController = SystemUiController(this)
|
||||||
statusBarColor = Color.TRANSPARENT
|
statusBarColor = Color.TRANSPARENT
|
||||||
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
|
||||||
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
|
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
|
||||||
} else {
|
} else {
|
||||||
Color.TRANSPARENT
|
Color.TRANSPARENT
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setTitle(title: CharSequence?) {
|
protected open fun setTitle(title: CharSequence?) {
|
||||||
(activity as? SettingsActivity)?.setSectionTitle(title)
|
(activity as? SettingsActivity)?.setSectionTitle(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,21 @@ import android.graphics.ColorFilter
|
|||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.animatorDurationScale
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
|
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
|
||||||
|
|
||||||
private val colorLow = context.getThemeColor(materialR.attr.colorBackgroundFloating)
|
private val colorLow = context.getThemeColor(materialR.attr.colorSurfaceContainerLowest)
|
||||||
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainer)
|
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainerHighest)
|
||||||
private var currentColor: Int = colorLow
|
private var currentColor: Int = colorLow
|
||||||
private var alpha: Int = 255
|
|
||||||
private val interpolator = FastOutSlowInInterpolator()
|
private val interpolator = FastOutSlowInInterpolator()
|
||||||
private val period = 2000 * context.animatorDurationScale
|
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||||
private val timeAnimator = TimeAnimator()
|
private val timeAnimator = TimeAnimator()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -32,7 +30,7 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
override fun draw(canvas: Canvas) {
|
||||||
if (!isRunning) {
|
if (!isRunning && period > 0) {
|
||||||
updateColor()
|
updateColor()
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
@@ -40,23 +38,22 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setAlpha(alpha: Int) {
|
override fun setAlpha(alpha: Int) {
|
||||||
this.alpha = alpha
|
// this.alpha = alpha FIXME coil's crossfade
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||||
throw UnsupportedOperationException("ColorFilter is not supported by PlaceholderDrawable")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||||
|
|
||||||
override fun getAlpha(): Int = alpha
|
override fun getAlpha(): Int = 255
|
||||||
|
|
||||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||||
if (callback != null) {
|
callback?.also {
|
||||||
updateColor()
|
updateColor()
|
||||||
invalidateSelf()
|
it.invalidateDrawable(this)
|
||||||
}
|
} ?: stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
@@ -64,19 +61,18 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
timeAnimator.cancel()
|
timeAnimator.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||||
|
|
||||||
private fun updateColor() {
|
private fun updateColor() {
|
||||||
|
if (period <= 0f) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val ph = period / 2
|
val ph = period / 2
|
||||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||||
var color = ArgbEvaluatorCompat.getInstance()
|
currentColor = ArgbEvaluatorCompat.getInstance()
|
||||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||||
if (alpha != 255) {
|
|
||||||
color = ColorUtils.setAlphaComponent(color, alpha)
|
|
||||||
}
|
|
||||||
currentColor = color
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.image
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.ColorFilter
|
|
||||||
import android.graphics.Outline
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Path
|
|
||||||
import android.graphics.PixelFormat
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.RectF
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.graphics.drawable.LayerDrawable
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.ReturnThis
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
|
||||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
class CardDrawable(
|
|
||||||
context: Context,
|
|
||||||
private var corners: Int,
|
|
||||||
) : Drawable() {
|
|
||||||
|
|
||||||
private val cornerSize = context.resources.resolveDp(12f)
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
||||||
private val cornersF = FloatArray(8)
|
|
||||||
private val boundsF = RectF()
|
|
||||||
private val color: ColorStateList
|
|
||||||
private val path = Path()
|
|
||||||
private var alpha = 255
|
|
||||||
private var state: IntArray? = null
|
|
||||||
private var horizontalInset: Int = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
paint.style = Paint.Style.FILL
|
|
||||||
color = context.getThemeColorStateList(materialR.attr.colorSurfaceContainerHighest)
|
|
||||||
?: ColorStateList.valueOf(Color.TRANSPARENT)
|
|
||||||
setCorners(corners)
|
|
||||||
updateColor()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
|
||||||
canvas.drawPath(path, paint)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setAlpha(alpha: Int) {
|
|
||||||
this.alpha = alpha
|
|
||||||
updateColor()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
|
||||||
paint.colorFilter = colorFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getColorFilter(): ColorFilter? = paint.colorFilter
|
|
||||||
|
|
||||||
override fun getOutline(outline: Outline) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
outline.setPath(path)
|
|
||||||
} else if (path.isConvex) {
|
|
||||||
outline.setConvexPath(path)
|
|
||||||
}
|
|
||||||
outline.alpha = 1f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPadding(padding: Rect): Boolean {
|
|
||||||
padding.set(
|
|
||||||
horizontalInset,
|
|
||||||
0,
|
|
||||||
horizontalInset,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if (corners or TOP != 0) {
|
|
||||||
padding.top += cornerSize.toIntUp()
|
|
||||||
}
|
|
||||||
if (corners or BOTTOM != 0) {
|
|
||||||
padding.bottom += cornerSize.toIntUp()
|
|
||||||
}
|
|
||||||
return horizontalInset != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChange(state: IntArray): Boolean {
|
|
||||||
this.state = state
|
|
||||||
if (color.isStateful) {
|
|
||||||
updateColor()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
|
|
||||||
|
|
||||||
override fun onBoundsChange(bounds: Rect) {
|
|
||||||
super.onBoundsChange(bounds)
|
|
||||||
boundsF.set(bounds)
|
|
||||||
boundsF.inset(horizontalInset.toFloat(), 0f)
|
|
||||||
path.reset()
|
|
||||||
path.addRoundRect(boundsF, cornersF, Path.Direction.CW)
|
|
||||||
path.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReturnThis
|
|
||||||
fun setCorners(corners: Int): CardDrawable {
|
|
||||||
this.corners = corners
|
|
||||||
val topLeft = if (corners and TOP_LEFT == TOP_LEFT) cornerSize else 0f
|
|
||||||
val topRight = if (corners and TOP_RIGHT == TOP_RIGHT) cornerSize else 0f
|
|
||||||
val bottomRight = if (corners and BOTTOM_RIGHT == BOTTOM_RIGHT) cornerSize else 0f
|
|
||||||
val bottomLeft = if (corners and BOTTOM_LEFT == BOTTOM_LEFT) cornerSize else 0f
|
|
||||||
cornersF[0] = topLeft
|
|
||||||
cornersF[1] = topLeft
|
|
||||||
cornersF[2] = topRight
|
|
||||||
cornersF[3] = topRight
|
|
||||||
cornersF[4] = bottomRight
|
|
||||||
cornersF[5] = bottomRight
|
|
||||||
cornersF[6] = bottomLeft
|
|
||||||
cornersF[7] = bottomLeft
|
|
||||||
invalidateSelf()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHorizontalInset(inset: Int) {
|
|
||||||
horizontalInset = inset
|
|
||||||
invalidateSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateColor() {
|
|
||||||
paint.color = color.getColorForState(state, color.defaultColor)
|
|
||||||
paint.alpha = alpha
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val TOP_LEFT = 1
|
|
||||||
const val TOP_RIGHT = 2
|
|
||||||
const val BOTTOM_LEFT = 4
|
|
||||||
const val BOTTOM_RIGHT = 8
|
|
||||||
|
|
||||||
const val LEFT = TOP_LEFT or BOTTOM_LEFT
|
|
||||||
const val TOP = TOP_LEFT or TOP_RIGHT
|
|
||||||
const val RIGHT = TOP_RIGHT or BOTTOM_RIGHT
|
|
||||||
const val BOTTOM = BOTTOM_LEFT or BOTTOM_RIGHT
|
|
||||||
|
|
||||||
const val NONE = 0
|
|
||||||
const val ALL = TOP_LEFT or TOP_RIGHT or BOTTOM_RIGHT or BOTTOM_LEFT
|
|
||||||
|
|
||||||
fun from(d: Drawable?): CardDrawable? = when (d) {
|
|
||||||
null -> null
|
|
||||||
is CardDrawable -> d
|
|
||||||
is LayerDrawable -> (0 until d.numberOfLayers).firstNotNullOfOrNull { i ->
|
|
||||||
from(d.getDrawable(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import coil.size.Dimension
|
import coil.size.Dimension
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.SizeResolver
|
import coil.size.ViewSizeResolver
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -16,24 +16,24 @@ private const val ASPECT_RATIO_HEIGHT = 18f
|
|||||||
private const val ASPECT_RATIO_WIDTH = 13f
|
private const val ASPECT_RATIO_WIDTH = 13f
|
||||||
|
|
||||||
class CoverSizeResolver(
|
class CoverSizeResolver(
|
||||||
private val imageView: ImageView,
|
override val view: ImageView,
|
||||||
) : SizeResolver {
|
) : ViewSizeResolver<ImageView> {
|
||||||
|
|
||||||
override suspend fun size(): Size {
|
override suspend fun size(): Size {
|
||||||
getSize()?.let { return it }
|
getSize()?.let { return it }
|
||||||
return suspendCancellableCoroutine { cont ->
|
return suspendCancellableCoroutine { cont ->
|
||||||
val layoutListener = LayoutListener(cont)
|
val layoutListener = LayoutListener(cont)
|
||||||
imageView.addOnLayoutChangeListener(layoutListener)
|
view.addOnLayoutChangeListener(layoutListener)
|
||||||
cont.invokeOnCancellation {
|
cont.invokeOnCancellation {
|
||||||
imageView.removeOnLayoutChangeListener(layoutListener)
|
view.removeOnLayoutChangeListener(layoutListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSize(): Size? {
|
private fun getSize(): Size? {
|
||||||
val lp = imageView.layoutParams
|
val lp = view.layoutParams
|
||||||
var width = getDimension(lp.width, imageView.width, imageView.paddingLeft + imageView.paddingRight)
|
var width = getDimension(lp.width, view.width, view.paddingLeft + view.paddingRight)
|
||||||
var height = getDimension(lp.height, imageView.height, imageView.paddingTop + imageView.paddingBottom)
|
var height = getDimension(lp.height, view.height, view.paddingTop + view.paddingBottom)
|
||||||
if (width == null && height == null) {
|
if (width == null && height == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||||||
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
super.setPadding(left, top, right, bottom)
|
||||||
|
fastScroller.setPadding(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||||
|
super.setPaddingRelative(start, top, end, bottom)
|
||||||
|
fastScroller.setPaddingRelative(start, top, end, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
fastScroller.attachRecyclerView(this)
|
fastScroller.attachRecyclerView(this)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
private var hideScrollbar = true
|
private var hideScrollbar = true
|
||||||
private var showBubble = true
|
private var showBubble = true
|
||||||
private var showBubbleAlways = false
|
private var showBubbleAlways = false
|
||||||
private var bubbleSize = BubbleSize.NORMAL
|
private var bubbleSize = BubbleSize.SMALL
|
||||||
private var bubbleImage: Drawable? = null
|
private var bubbleImage: Drawable? = null
|
||||||
private var handleImage: Drawable? = null
|
private var handleImage: Drawable? = null
|
||||||
private var trackImage: Drawable? = null
|
private var trackImage: Drawable? = null
|
||||||
@@ -91,7 +91,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
|
|
||||||
if (showBubbleAlways) {
|
if (showBubbleAlways) {
|
||||||
val targetPos = getRecyclerViewTargetPosition(y)
|
val targetPos = getRecyclerViewTargetPosition(y)
|
||||||
sectionIndexer?.let { binding.bubble.text = it.getSectionText(recyclerView.context, targetPos) }
|
sectionIndexer?.let { bindBubble(it.getSectionText(recyclerView.context, targetPos)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
showBubble = getBoolean(R.styleable.FastScrollRecyclerView_showBubble, showBubble)
|
showBubble = getBoolean(R.styleable.FastScrollRecyclerView_showBubble, showBubble)
|
||||||
showBubbleAlways = getBoolean(R.styleable.FastScrollRecyclerView_showBubbleAlways, showBubbleAlways)
|
showBubbleAlways = getBoolean(R.styleable.FastScrollRecyclerView_showBubbleAlways, showBubbleAlways)
|
||||||
showTrack = getBoolean(R.styleable.FastScrollRecyclerView_showTrack, showTrack)
|
showTrack = getBoolean(R.styleable.FastScrollRecyclerView_showTrack, showTrack)
|
||||||
bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, BubbleSize.NORMAL)
|
bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, bubbleSize)
|
||||||
val textSize = getDimension(R.styleable.FastScrollRecyclerView_bubbleTextSize, bubbleSize.textSize)
|
val textSize = getDimension(R.styleable.FastScrollRecyclerView_bubbleTextSize, bubbleSize.textSize)
|
||||||
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
offset = getDimensionPixelOffset(R.styleable.FastScrollRecyclerView_scrollerOffset, offset)
|
offset = getDimensionPixelOffset(R.styleable.FastScrollRecyclerView_scrollerOffset, offset)
|
||||||
@@ -473,7 +473,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
val layoutManager = recyclerView?.layoutManager ?: return
|
val layoutManager = recyclerView?.layoutManager ?: return
|
||||||
val targetPos = getRecyclerViewTargetPosition(y)
|
val targetPos = getRecyclerViewTargetPosition(y)
|
||||||
layoutManager.scrollToPosition(targetPos)
|
layoutManager.scrollToPosition(targetPos)
|
||||||
if (showBubble) sectionIndexer?.let { binding.bubble.text = it.getSectionText(context, targetPos) }
|
if (showBubble) sectionIndexer?.let { bindBubble(it.getSectionText(context, targetPos)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setViewPositions(y: Float) {
|
private fun setViewPositions(y: Float) {
|
||||||
@@ -535,6 +535,11 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bindBubble(text: CharSequence?) {
|
||||||
|
binding.bubble.text = text
|
||||||
|
binding.bubble.alpha = if (text.isNullOrEmpty()) 0f else 1f
|
||||||
|
}
|
||||||
|
|
||||||
private val BubbleSize.textSize
|
private val BubbleSize.textSize
|
||||||
@Px get() = resources.getDimension(textSizeId)
|
@Px get() = resources.getDimension(textSizeId)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package org.koitharu.kotatsu.core.ui.sheet
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -16,15 +14,8 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.appcompat.app.AppCompatDialog
|
import androidx.appcompat.app.AppCompatDialog
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.ActionBarContextView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
@@ -33,14 +24,12 @@ import com.google.android.material.sidesheet.SideSheetDialog
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||||
|
|
||||||
private var waitingForDismissAllowingStateLoss = false
|
private var waitingForDismissAllowingStateLoss = false
|
||||||
private var isFitToContentsDisabled = false
|
private var isFitToContentsDisabled = false
|
||||||
private var defaultStatusBarColor = Color.TRANSPARENT
|
|
||||||
|
|
||||||
var viewBinding: B? = null
|
var viewBinding: B? = null
|
||||||
private set
|
private set
|
||||||
@@ -105,40 +94,18 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
|
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
|
||||||
actionModeDelegate?.onSupportActionModeStarted(mode)
|
actionModeDelegate?.onSupportActionModeStarted(mode, dialog?.window)
|
||||||
val ctx = requireContext()
|
|
||||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
ColorUtils.compositeColors(
|
|
||||||
ContextCompat.getColor(ctx, com.google.android.material.R.color.m3_appbar_overlay_color),
|
|
||||||
ctx.getThemeColor(com.google.android.material.R.attr.colorSurface),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
|
|
||||||
}
|
|
||||||
dialog?.window?.let {
|
|
||||||
defaultStatusBarColor = it.statusBarColor
|
|
||||||
it.statusBarColor = actionModeColor
|
|
||||||
}
|
|
||||||
val insets = ViewCompat.getRootWindowInsets(requireView())
|
|
||||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
|
||||||
dialog?.window?.decorView?.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
|
|
||||||
setBackgroundColor(actionModeColor)
|
|
||||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = insets.top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
|
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
|
||||||
actionModeDelegate?.onSupportActionModeFinished(mode)
|
actionModeDelegate?.onSupportActionModeFinished(mode, dialog?.window)
|
||||||
dialog?.window?.statusBarColor = defaultStatusBarColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
||||||
val b = behavior ?: return false
|
val b = behavior ?: return false
|
||||||
b.addCallback(callback)
|
b.addCallback(callback)
|
||||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet)
|
||||||
?: dialog?.findViewById(materialR.id.coordinator)
|
?: dialog?.findViewById(materialR.id.coordinator)
|
||||||
?: view
|
?: view
|
||||||
if (rootView != null) {
|
if (rootView != null) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.BackEventCompat
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
|
class BottomSheetCollapseCallback(
|
||||||
|
private val sheet: ViewGroup,
|
||||||
|
private val behavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(sheet),
|
||||||
|
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED || behavior.state == STATE_HALF_EXPANDED) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
behavior.addBottomSheetCallback(
|
||||||
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
|
||||||
|
@SuppressLint("SwitchIntDef")
|
||||||
|
override fun onStateChanged(view: View, state: Int) {
|
||||||
|
when (state) {
|
||||||
|
STATE_EXPANDED,
|
||||||
|
STATE_HALF_EXPANDED -> isEnabled = true
|
||||||
|
|
||||||
|
STATE_COLLAPSED,
|
||||||
|
STATE_HIDDEN -> isEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(p0: View, p1: Float) = Unit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleOnBackPressed() = behavior.handleBackInvoked()
|
||||||
|
|
||||||
|
override fun handleOnBackCancelled() = behavior.cancelBackProgress()
|
||||||
|
|
||||||
|
override fun handleOnBackProgressed(backEvent: BackEventCompat) = behavior.updateBackProgress(backEvent)
|
||||||
|
|
||||||
|
override fun handleOnBackStarted(backEvent: BackEventCompat) = behavior.startBackProgress(backEvent)
|
||||||
|
}
|
||||||
@@ -1,14 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.util
|
package org.koitharu.kotatsu.core.ui.util
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.appcompat.widget.ActionBarContextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class ActionModeDelegate : OnBackPressedCallback(false) {
|
class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||||
|
|
||||||
private var activeActionMode: ActionMode? = null
|
private var activeActionMode: ActionMode? = null
|
||||||
private var listeners: MutableList<ActionModeListener>? = null
|
private var listeners: MutableList<ActionModeListener>? = null
|
||||||
|
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||||
|
|
||||||
val isActionModeStarted: Boolean
|
val isActionModeStarted: Boolean
|
||||||
get() = activeActionMode != null
|
get() = activeActionMode != null
|
||||||
@@ -17,16 +31,40 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
|||||||
finishActionMode()
|
finishActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
fun onSupportActionModeStarted(mode: ActionMode, window: Window?) {
|
||||||
activeActionMode = mode
|
activeActionMode = mode
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
listeners?.forEach { it.onActionModeStarted(mode) }
|
listeners?.forEach { it.onActionModeStarted(mode) }
|
||||||
|
if (window != null) {
|
||||||
|
val ctx = window.context
|
||||||
|
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
ColorUtils.compositeColors(
|
||||||
|
ContextCompat.getColor(ctx, materialR.color.m3_appbar_overlay_color),
|
||||||
|
ctx.getThemeColor(materialR.attr.colorSurface),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
|
||||||
|
}
|
||||||
|
defaultStatusBarColor = window.statusBarColor
|
||||||
|
window.statusBarColor = actionModeColor
|
||||||
|
val insets = ViewCompat.getRootWindowInsets(window.decorView)
|
||||||
|
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||||
|
window.decorView.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
|
||||||
|
setBackgroundColor(actionModeColor)
|
||||||
|
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSupportActionModeFinished(mode: ActionMode) {
|
fun onSupportActionModeFinished(mode: ActionMode, window: Window?) {
|
||||||
activeActionMode = null
|
activeActionMode = null
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
listeners?.forEach { it.onActionModeFinished(mode) }
|
listeners?.forEach { it.onActionModeFinished(mode) }
|
||||||
|
if (window != null) {
|
||||||
|
window.statusBarColor = defaultStatusBarColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addListener(listener: ActionModeListener) {
|
fun addListener(listener: ActionModeListener) {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.util
|
|
||||||
|
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
|
|
||||||
@EntryPoint
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
interface BaseActivityEntryPoint {
|
|
||||||
val settings: AppSettings
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.util
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
|
||||||
|
|
||||||
class BottomSheetClollapseCallback(
|
|
||||||
private val behavior: BottomSheetBehavior<*>,
|
|
||||||
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
behavior.addBottomSheetCallback(
|
|
||||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
|
|
||||||
override fun onStateChanged(view: View, state: Int) {
|
|
||||||
isEnabled = state == STATE_EXPANDED || state == STATE_HALF_EXPANDED
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSlide(p0: View, p1: Float) = Unit
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
behavior.state = STATE_COLLAPSED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.util
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.ancestors
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.Lifecycle.State.RESUMED
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
|
||||||
|
class PagerNestedScrollHelper(
|
||||||
|
private val recyclerView: RecyclerView,
|
||||||
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
fun bind(lifecycleOwner: LifecycleOwner) {
|
||||||
|
lifecycleOwner.lifecycle.addObserver(this)
|
||||||
|
recyclerView.isNestedScrollingEnabled = lifecycleOwner.lifecycle.currentState.isAtLeast(RESUMED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
|
recyclerView.isNestedScrollingEnabled = false
|
||||||
|
invalidateBottomSheetScrollTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
recyclerView.isNestedScrollingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
owner.lifecycle.removeObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we need to invalidate the `nestedScrollingChildRef` of the [BottomSheetBehavior]
|
||||||
|
*/
|
||||||
|
private fun invalidateBottomSheetScrollTarget() {
|
||||||
|
var handleCoordinator = false
|
||||||
|
for (parent in recyclerView.ancestors) {
|
||||||
|
if (handleCoordinator && parent is CoordinatorLayout) {
|
||||||
|
parent.requestLayout()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val lp = (parent as? View)?.layoutParams ?: continue
|
||||||
|
if (lp is CoordinatorLayout.LayoutParams && lp.behavior is BottomSheetBehavior<*>) {
|
||||||
|
handleCoordinator = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.util
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener {
|
|
||||||
|
|
||||||
private var animator: ValueAnimator? = null
|
|
||||||
private val interpolator = AccelerateDecelerateInterpolator()
|
|
||||||
|
|
||||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
|
||||||
val foreground = appBarLayout.statusBarForeground ?: return
|
|
||||||
val start = foreground.alpha
|
|
||||||
val collapsed = verticalOffset != 0
|
|
||||||
val end = if (collapsed) 255 else 0
|
|
||||||
animator?.cancel()
|
|
||||||
if (start == end) {
|
|
||||||
animator = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
animator = ValueAnimator.ofInt(start, end).apply {
|
|
||||||
duration = appBarLayout.context.getAnimationDuration(materialR.integer.app_bar_elevation_anim_duration)
|
|
||||||
interpolator = this@StatusBarDimHelper.interpolator
|
|
||||||
addUpdateListener {
|
|
||||||
foreground.alpha = it.animatedValue as Int
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun attachToAppBar(appBarLayout: AppBarLayout) {
|
|
||||||
appBarLayout.addOnOffsetChangedListener(this)
|
|
||||||
appBarLayout.statusBarForeground =
|
|
||||||
MaterialShapeDrawable.createWithElevationOverlay(appBarLayout.context).apply {
|
|
||||||
alpha = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,23 +33,30 @@ sealed class SystemUiController(
|
|||||||
private class LegacyImpl(window: Window) : SystemUiController(window) {
|
private class LegacyImpl(window: Window) : SystemUiController(window) {
|
||||||
|
|
||||||
override fun setSystemUiVisible(value: Boolean) {
|
override fun setSystemUiVisible(value: Boolean) {
|
||||||
|
val flags = window.decorView.systemUiVisibility
|
||||||
window.decorView.systemUiVisibility = if (value) {
|
window.decorView.systemUiVisibility = if (value) {
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
(flags and LEGACY_FLAGS_HIDDEN.inv()) or LEGACY_FLAGS_VISIBLE
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
} else {
|
} else {
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
(flags and LEGACY_FLAGS_VISIBLE.inv()) or LEGACY_FLAGS_HIDDEN
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private const val LEGACY_FLAGS_VISIBLE = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private const val LEGACY_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
|
||||||
operator fun invoke(window: Window): SystemUiController =
|
operator fun invoke(window: Window): SystemUiController =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
Api30Impl(window)
|
Api30Impl(window)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.google.android.material.chip.ChipGroup
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
||||||
|
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class ChipsView @JvmOverloads constructor(
|
class ChipsView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -24,7 +26,9 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
||||||
}
|
}
|
||||||
private val chipOnCloseListener = OnClickListener {
|
private val chipOnCloseListener = OnClickListener {
|
||||||
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
|
val chip = it as Chip
|
||||||
|
val data = it.tag
|
||||||
|
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
|
||||||
}
|
}
|
||||||
private val chipStyle: Int
|
private val chipStyle: Int
|
||||||
var onChipClickListener: OnChipClickListener? = null
|
var onChipClickListener: OnChipClickListener? = null
|
||||||
@@ -48,7 +52,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
if (isInEditMode) {
|
if (isInEditMode) {
|
||||||
setChips(
|
setChips(
|
||||||
List(5) {
|
List(5) {
|
||||||
ChipModel(0, "Chip $it", 0, isCheckable = false, isChecked = false)
|
ChipModel(title = "Chip $it")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -99,6 +103,15 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
chip.isChipIconVisible = true
|
chip.isChipIconVisible = true
|
||||||
}
|
}
|
||||||
chip.isChecked = model.isChecked
|
chip.isChecked = model.isChecked
|
||||||
|
chip.isCheckedIconVisible = chip.isCheckable && model.icon == 0
|
||||||
|
chip.isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
|
||||||
|
chip.setCloseIconResource(
|
||||||
|
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
|
||||||
|
)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
chip.tag = model.data
|
chip.tag = model.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +119,11 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
val chip = Chip(context)
|
val chip = Chip(context)
|
||||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
||||||
chip.setChipDrawable(drawable)
|
chip.setChipDrawable(drawable)
|
||||||
chip.isCheckedIconVisible = true
|
|
||||||
chip.isChipIconVisible = false
|
chip.isChipIconVisible = false
|
||||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
|
||||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||||
chip.setEnsureMinTouchTargetSize(false)
|
chip.setEnsureMinTouchTargetSize(false)
|
||||||
chip.setOnClickListener(chipOnClickListener)
|
chip.setOnClickListener(chipOnClickListener)
|
||||||
|
chip.isElegantTextHeight = false
|
||||||
addView(chip)
|
addView(chip)
|
||||||
return chip
|
return chip
|
||||||
}
|
}
|
||||||
@@ -127,11 +139,12 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ChipModel(
|
data class ChipModel(
|
||||||
@ColorRes val tint: Int,
|
|
||||||
val title: CharSequence,
|
val title: CharSequence,
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int = 0,
|
||||||
val isCheckable: Boolean,
|
val isCheckable: Boolean = false,
|
||||||
val isChecked: Boolean,
|
@ColorRes val tint: Int = 0,
|
||||||
|
val isChecked: Boolean = false,
|
||||||
|
val isDropdown: Boolean = false,
|
||||||
val data: Any? = null,
|
val data: Any? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.widgets
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.viewpager.widget.ViewPager
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
class EnhancedViewPager @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
) : ViewPager(context, attrs) {
|
|
||||||
|
|
||||||
var isUserInputEnabled: Boolean = true
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (!value) {
|
|
||||||
cancelPendingInputEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
return isUserInputEnabled && super.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
return try {
|
|
||||||
isUserInputEnabled && super.onInterceptTouchEvent(event)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
e.printStackTraceDebug()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import com.google.android.material.textview.MaterialTextView
|
||||||
|
|
||||||
|
class MultilineEllipsizeTextView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
@AttrRes defStyleAttr: Int = android.R.attr.textViewStyle,
|
||||||
|
) : MaterialTextView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
val lh = lineHeight
|
||||||
|
maxLines = if (lh > 0) h / lh else 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,13 @@ import android.widget.TextView
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.widget.LinearLayoutCompat
|
import androidx.appcompat.widget.LinearLayoutCompat
|
||||||
import androidx.core.content.withStyledAttributes
|
import androidx.core.content.withStyledAttributes
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
@@ -37,10 +39,14 @@ class ProgressButton @JvmOverloads constructor(
|
|||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
private var progress = 0f
|
private var progress = 0f
|
||||||
|
private var targetProgress = 0f
|
||||||
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||||
private var colorProgress: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
private var colorProgress: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||||
private var progressAnimator: ValueAnimator? = null
|
private var progressAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
private var colorBaseCurrent = colorProgress.defaultColor
|
||||||
|
private var colorProgressCurrent = colorProgress.defaultColor
|
||||||
|
|
||||||
var title: CharSequence?
|
var title: CharSequence?
|
||||||
get() = textViewTitle.textAndVisible
|
get() = textViewTitle.textAndVisible
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -97,10 +103,19 @@ class ProgressButton @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
canvas.drawColor(colorBase.getColorForState(drawableState, colorBase.defaultColor))
|
canvas.drawColor(colorBaseCurrent)
|
||||||
paint.color = colorProgress.getColorForState(drawableState, colorProgress.defaultColor)
|
if (progress > 0f) {
|
||||||
paint.alpha = 84 // 255 * 0.33F
|
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
|
||||||
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun drawableStateChanged() {
|
||||||
|
super.drawableStateChanged()
|
||||||
|
val state = drawableState
|
||||||
|
colorBaseCurrent = colorBase.getColorForState(state, colorBase.defaultColor)
|
||||||
|
colorProgressCurrent = colorProgress.getColorForState(state, colorProgress.defaultColor)
|
||||||
|
colorProgressCurrent = ColorUtils.setAlphaComponent(colorProgressCurrent, 84 /* 255 * 0.33F */)
|
||||||
|
paint.color = colorProgressCurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setGravity(gravity: Int) {
|
override fun setGravity(gravity: Int) {
|
||||||
@@ -116,8 +131,10 @@ class ProgressButton @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationUpdate(animation: ValueAnimator) {
|
override fun onAnimationUpdate(animation: ValueAnimator) {
|
||||||
progress = animation.animatedValue as Float
|
if (animation === progressAnimator) {
|
||||||
invalidate()
|
progress = animation.animatedValue as Float
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTitle(@StringRes titleResId: Int) {
|
fun setTitle(@StringRes titleResId: Int) {
|
||||||
@@ -129,19 +146,25 @@ class ProgressButton @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setProgress(value: Float, animate: Boolean) {
|
fun setProgress(value: Float, animate: Boolean) {
|
||||||
progressAnimator?.cancel()
|
val prevAnimator = progressAnimator
|
||||||
if (animate) {
|
if (animate && context.isAnimationsEnabled) {
|
||||||
|
if (value == targetProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetProgress = value
|
||||||
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
|
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
|
||||||
duration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
|
duration = context.getAnimationDuration(android.R.integer.config_mediumAnimTime)
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
addUpdateListener(this@ProgressButton)
|
addUpdateListener(this@ProgressButton)
|
||||||
start()
|
|
||||||
}
|
}
|
||||||
|
progressAnimator?.start()
|
||||||
} else {
|
} else {
|
||||||
progressAnimator = null
|
progressAnimator = null
|
||||||
progress = value
|
progress = value
|
||||||
|
targetProgress = value
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
prevAnimator?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyGravity() {
|
private fun applyGravity() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.view.ViewPropertyAnimator
|
|||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.customview.view.AbsSavedState
|
import androidx.customview.view.AbsSavedState
|
||||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||||
@@ -47,6 +48,9 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isShownOrShowing: Boolean
|
||||||
|
get() = isVisible && currentState == STATE_UP
|
||||||
|
|
||||||
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
||||||
return behavior
|
return behavior
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.map
|
import org.koitharu.kotatsu.core.util.ext.iterator
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class LocaleComparator : Comparator<Locale> {
|
class LocaleComparator : Comparator<Locale> {
|
||||||
|
|
||||||
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
|
private val deviceLocales: List<String>
|
||||||
.map { it.language }
|
|
||||||
.distinct()
|
init {
|
||||||
|
val localeList = LocaleListCompat.getAdjustedDefault()
|
||||||
|
deviceLocales = buildList(localeList.size() + 1) {
|
||||||
|
add("")
|
||||||
|
val set = HashSet<String>(localeList.size() + 1)
|
||||||
|
set.add("")
|
||||||
|
for (locale in localeList) {
|
||||||
|
val lang = locale.language
|
||||||
|
if (set.add(lang)) {
|
||||||
|
add(lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun compare(a: Locale, b: Locale): Int {
|
override fun compare(a: Locale, b: Locale): Int {
|
||||||
val indexA = deviceLocales.indexOf(a.language)
|
val indexA = deviceLocales.indexOf(a.language)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.core.util
|
|||||||
|
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
class MultiMutex<T : Any> : Set<T> {
|
class MultiMutex<T : Any> : Set<T> {
|
||||||
|
|
||||||
@@ -10,12 +12,12 @@ class MultiMutex<T : Any> : Set<T> {
|
|||||||
override val size: Int
|
override val size: Int
|
||||||
get() = delegates.size
|
get() = delegates.size
|
||||||
|
|
||||||
override fun contains(element: T): Boolean {
|
override fun contains(element: T): Boolean = synchronized(delegates) {
|
||||||
return delegates.containsKey(element)
|
delegates.containsKey(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun containsAll(elements: Collection<T>): Boolean {
|
override fun containsAll(elements: Collection<T>): Boolean = synchronized(delegates) {
|
||||||
return elements.all { x -> delegates.containsKey(x) }
|
elements.all { x -> delegates.containsKey(x) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isEmpty(): Boolean {
|
override fun isEmpty(): Boolean {
|
||||||
@@ -40,4 +42,16 @@ class MultiMutex<T : Any> : Set<T> {
|
|||||||
delegates.remove(element)?.unlock()
|
delegates.remove(element)?.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend inline fun <R> withLock(element: T, block: () -> R): R {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
lock(element)
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
unlock(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.content.pm.ResolveInfo
|
|||||||
import android.database.SQLException
|
import android.database.SQLException
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -37,11 +38,14 @@ import androidx.appcompat.app.AppCompatDialog
|
|||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
|
import androidx.webkit.WebViewCompat
|
||||||
|
import androidx.webkit.WebViewFeature
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
import com.google.android.material.elevation.ElevationOverlayProvider
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -59,7 +63,6 @@ import okio.use
|
|||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.jsoup.internal.StringUtil.StringJoiner
|
import org.jsoup.internal.StringUtil.StringJoiner
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
@@ -73,6 +76,9 @@ val Context.activityManager: ActivityManager?
|
|||||||
val Context.powerManager: PowerManager?
|
val Context.powerManager: PowerManager?
|
||||||
get() = getSystemService(POWER_SERVICE) as? PowerManager
|
get() = getSystemService(POWER_SERVICE) as? PowerManager
|
||||||
|
|
||||||
|
val Context.connectivityManager: ConnectivityManager
|
||||||
|
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
||||||
|
|
||||||
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
|
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
|
||||||
@@ -139,6 +145,9 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
|
|||||||
!context.getSystemBoolean("config_navBarNeedsScrim", true)
|
!context.getSystemBoolean("config_navBarNeedsScrim", true)
|
||||||
) {
|
) {
|
||||||
Color.TRANSPARENT
|
Color.TRANSPARENT
|
||||||
|
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
|
||||||
|
val baseColor = context.getThemeColor(android.R.attr.navigationBarColor)
|
||||||
|
ColorUtils.setAlphaComponent(baseColor, (Color.alpha(baseColor) * alphaFactor).toInt())
|
||||||
} else {
|
} else {
|
||||||
// Set navbar scrim 70% of navigationBarColor
|
// Set navbar scrim 70% of navigationBarColor
|
||||||
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
|
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
|
||||||
@@ -263,6 +272,9 @@ fun WebView.configureForParser(userAgentOverride: String?) = with(settings) {
|
|||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
domStorageEnabled = true
|
domStorageEnabled = true
|
||||||
mediaPlaybackRequiresUserGesture = false
|
mediaPlaybackRequiresUserGesture = false
|
||||||
|
if (WebViewFeature.isFeatureSupported(WebViewFeature.MUTE_AUDIO)) {
|
||||||
|
WebViewCompat.setAudioMuted(this@configureForParser, true)
|
||||||
|
}
|
||||||
databaseEnabled = true
|
databaseEnabled = true
|
||||||
if (userAgentOverride != null) {
|
if (userAgentOverride != null) {
|
||||||
userAgentString = userAgentOverride
|
userAgentString = userAgentOverride
|
||||||
|
|||||||
@@ -47,15 +47,6 @@ fun ImageResult.getDrawableOrThrow() = when (this) {
|
|||||||
is ErrorResult -> throw throwable
|
is ErrorResult -> throw throwable
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"",
|
|
||||||
ReplaceWith(
|
|
||||||
"getDrawableOrThrow().toBitmap()",
|
|
||||||
"androidx.core.graphics.drawable.toBitmap",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
fun ImageResult.requireBitmap() = getDrawableOrThrow().toBitmap()
|
|
||||||
|
|
||||||
fun ImageResult.toBitmapOrNull() = when (this) {
|
fun ImageResult.toBitmapOrNull() = when (this) {
|
||||||
is SuccessResult -> try {
|
is SuccessResult -> try {
|
||||||
drawable.toBitmap()
|
drawable.toBitmap()
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.view.Display
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val Activity.displayCompat: Display
|
|
||||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
display ?: windowManager.defaultDisplay
|
|
||||||
} else {
|
|
||||||
windowManager.defaultDisplay
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.getDisplaySize(): Rect {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
windowManager.currentWindowMetrics.bounds
|
|
||||||
} else {
|
|
||||||
val dm = DisplayMetrics()
|
|
||||||
displayCompat.getRealMetrics(dm)
|
|
||||||
Rect(0, 0, dm.widthPixels, dm.heightPixels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -28,11 +27,16 @@ fun <T> Flow<T>.observe(owner: LifecycleOwner, minState: Lifecycle.State, collec
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
||||||
|
observeEvent(owner, Lifecycle.State.STARTED, collector)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, minState: Lifecycle.State, collector: FlowCollector<T>) {
|
||||||
owner.lifecycleScope.launch {
|
owner.lifecycleScope.launch {
|
||||||
owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
owner.repeatOnLifecycle(minState) {
|
||||||
collect {
|
collect {
|
||||||
it?.consume(collector)
|
it?.consume(collector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import okhttp3.internal.isSensitiveHeader
|
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.jsoup.HttpStatusException
|
import org.jsoup.HttpStatusException
|
||||||
@@ -42,6 +41,8 @@ fun Response.ensureSuccess() = apply {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Response.requireBody(): ResponseBody = checkNotNull(body) { "Response body is null" }
|
||||||
|
|
||||||
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
|
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
|
||||||
c.name(name)
|
c.name(name)
|
||||||
c.value(value)
|
c.value(value)
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementE
|
|||||||
|
|
||||||
fun String.toLocale() = Locale(this)
|
fun String.toLocale() = Locale(this)
|
||||||
|
|
||||||
fun Locale?.getDisplayName(context: Context): String {
|
fun Locale?.getDisplayName(context: Context): String = when (this) {
|
||||||
if (this == null) {
|
null -> context.getString(R.string.all_languages)
|
||||||
return context.getString(R.string.various_languages)
|
Locale.ROOT -> context.getString(R.string.various_languages)
|
||||||
}
|
else -> getDisplayLanguage(this).toTitleCase(this)
|
||||||
return getDisplayLanguage(this).toTitleCase(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
|
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
val Context.connectivityManager: ConnectivityManager
|
|
||||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
|
|
||||||
fun ConnectivityManager.isOnline(): Boolean {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
activeNetwork?.let { isOnline(it) } ?: false
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
activeNetworkInfo?.isConnected == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ConnectivityManager.isOnline(network: Network): Boolean {
|
|
||||||
val capabilities = getNetworkCapabilities(network)
|
|
||||||
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
}
|
|
||||||
@@ -54,14 +54,13 @@ class DetailsLoadUseCase @Inject constructor(
|
|||||||
send(MangaDetails(manga, null, null, false))
|
send(MangaDetails(manga, null, null, false))
|
||||||
try {
|
try {
|
||||||
val details = getDetails(manga)
|
val details = getDetails(manga)
|
||||||
launch { updateTracker(manga) }
|
launch { updateTracker(details) }
|
||||||
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||||
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
local?.await()?.manga?.also { localManga ->
|
local?.await()?.manga?.also { localManga ->
|
||||||
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
|
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
|
||||||
}
|
} ?: close(e)
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.model.findById
|
import org.koitharu.kotatsu.core.model.findById
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
@@ -27,7 +27,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
|||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cache: ContentCache
|
lateinit var cache: MemoryContentCache
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var historyRepository: HistoryRepository
|
lateinit var historyRepository: HistoryRepository
|
||||||
@@ -110,17 +110,14 @@ class MangaPrefetchService : CoroutineIntentService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||||
if (source == MangaSource.LOCAL) {
|
if (source == MangaSource.LOCAL || context.isPowerSaveMode()) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (context.isPowerSaveMode()) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val entryPoint = EntryPointAccessors.fromApplication(
|
val entryPoint = EntryPointAccessors.fromApplication(
|
||||||
context,
|
context,
|
||||||
PrefetchCompanionEntryPoint::class.java,
|
PrefetchCompanionEntryPoint::class.java,
|
||||||
)
|
)
|
||||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
|
return entryPoint.settings.isContentPrefetchEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryStart(context: Context, intent: Intent) {
|
private fun tryStart(context: Context, intent: Intent) {
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package org.koitharu.kotatsu.details.service
|
|||||||
import dagger.hilt.EntryPoint
|
import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
@EntryPoint
|
@EntryPoint
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface PrefetchCompanionEntryPoint {
|
interface PrefetchCompanionEntryPoint {
|
||||||
val settings: AppSettings
|
val settings: AppSettings
|
||||||
val contentCache: ContentCache
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ fun MangaDetails.mapChapters(
|
|||||||
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||||
var prevVolume = 0
|
var prevVolume = 0
|
||||||
val result = ArrayList<ListModel>((size * 1.4).toInt())
|
val result = ArrayList<ListModel>((size * 1.4).toInt())
|
||||||
var groupPos: Byte = 0
|
for (item in this) {
|
||||||
for ((index, item) in this.withIndex()) {
|
|
||||||
val chapter = item.chapter
|
val chapter = item.chapter
|
||||||
if (chapter.volume != prevVolume) {
|
if (chapter.volume != prevVolume) {
|
||||||
val text = if (chapter.volume == 0) {
|
val text = if (chapter.volume == 0) {
|
||||||
@@ -83,19 +82,8 @@ fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
|||||||
}
|
}
|
||||||
result.add(ListHeader(text))
|
result.add(ListHeader(text))
|
||||||
prevVolume = chapter.volume
|
prevVolume = chapter.volume
|
||||||
groupPos = ChapterListItem.GROUP_START
|
|
||||||
} else if (groupPos == ChapterListItem.GROUP_START) {
|
|
||||||
groupPos = ChapterListItem.GROUP_MIDDLE
|
|
||||||
}
|
|
||||||
if (groupPos != 0.toByte()) {
|
|
||||||
val next = this.getOrNull(index + 1)
|
|
||||||
if (next == null || next.chapter.volume != prevVolume) {
|
|
||||||
groupPos = ChapterListItem.GROUP_END
|
|
||||||
}
|
|
||||||
result.add(item.copy(groupPosition = groupPos))
|
|
||||||
} else {
|
|
||||||
result.add(item)
|
|
||||||
}
|
}
|
||||||
|
result.add(item)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ import coil.request.ImageRequest
|
|||||||
import coil.request.SuccessResult
|
import coil.request.SuccessResult
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filterNot
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
@@ -53,12 +54,11 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
|||||||
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback
|
import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback
|
||||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.util.FileSize
|
import org.koitharu.kotatsu.core.util.FileSize
|
||||||
import org.koitharu.kotatsu.core.util.ViewBadge
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
@@ -124,7 +124,6 @@ class DetailsActivity :
|
|||||||
|
|
||||||
private val viewModel: DetailsViewModel by viewModels()
|
private val viewModel: DetailsViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var chaptersBadge: ViewBadge
|
|
||||||
private lateinit var menuProvider: DetailsMenuProvider
|
private lateinit var menuProvider: DetailsMenuProvider
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -155,14 +154,12 @@ class DetailsActivity :
|
|||||||
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||||
viewBinding.chipsTags.onChipClickListener = this
|
viewBinding.chipsTags.onChipClickListener = this
|
||||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||||
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
|
viewBinding.containerBottomSheet?.let { sheet ->
|
||||||
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
|
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet))
|
||||||
}
|
}
|
||||||
chaptersBadge = ViewBadge(viewBinding.buttonRead, this)
|
|
||||||
|
|
||||||
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
||||||
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
|
||||||
viewModel.onError
|
viewModel.onError
|
||||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||||
.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
|
.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
|
||||||
@@ -185,7 +182,8 @@ class DetailsActivity :
|
|||||||
viewModel.isStatsAvailable.observe(this, menuInvalidator)
|
viewModel.isStatsAvailable.observe(this, menuInvalidator)
|
||||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||||
viewModel.branches.observe(this) {
|
viewModel.branches.observe(this) {
|
||||||
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1
|
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty()
|
||||||
|
viewBinding.infoLayout.chipBranch.isCloseIconVisible = it.size > 1
|
||||||
}
|
}
|
||||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||||
viewModel.onDownloadStarted
|
viewModel.onDownloadStarted
|
||||||
@@ -201,6 +199,8 @@ class DetailsActivity :
|
|||||||
addMenuProvider(menuProvider)
|
addMenuProvider(menuProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isNsfwContent(): Flow<Boolean> = viewModel.manga.map { it?.isNsfw == true }
|
||||||
|
|
||||||
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)
|
||||||
@@ -379,15 +379,6 @@ class DetailsActivity :
|
|||||||
chip.textAndVisible = time?.formatShort(chip.resources)
|
chip.textAndVisible = time?.formatShort(chip.resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDescriptionChanged(description: CharSequence?) {
|
|
||||||
val tv = viewBinding.textViewDescription
|
|
||||||
if (description.isNullOrBlank()) {
|
|
||||||
tv.setText(R.string.no_description)
|
|
||||||
} else {
|
|
||||||
tv.text = description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLocalSizeChanged(size: Long) {
|
private fun onLocalSizeChanged(size: Long) {
|
||||||
val chip = viewBinding.infoLayout.chipSize
|
val chip = viewBinding.infoLayout.chipSize
|
||||||
if (size == 0L) {
|
if (size == 0L) {
|
||||||
@@ -455,7 +446,7 @@ class DetailsActivity :
|
|||||||
loadCover(manga)
|
loadCover(manga)
|
||||||
textViewTitle.text = manga.title
|
textViewTitle.text = manga.title
|
||||||
textViewSubtitle.textAndVisible = manga.altTitle
|
textViewSubtitle.textAndVisible = manga.altTitle
|
||||||
infoLayout.chipAuthor.textAndVisible = manga.author
|
infoLayout.chipAuthor.textAndVisible = manga.author?.ellipsize(AUTHOR_LABEL_LIMIT)
|
||||||
if (manga.hasRating) {
|
if (manga.hasRating) {
|
||||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||||
ratingBar.isVisible = true
|
ratingBar.isVisible = true
|
||||||
@@ -545,18 +536,19 @@ class DetailsActivity :
|
|||||||
info.totalChapters == -1 -> getString(R.string.error_occurred)
|
info.totalChapters == -1 -> getString(R.string.error_occurred)
|
||||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||||
}
|
}
|
||||||
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true)
|
val isFirstCall = buttonRead.tag == null
|
||||||
|
buttonRead.tag = Unit
|
||||||
|
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, !isFirstCall)
|
||||||
buttonDownload?.isEnabled = info.isValid && info.canDownload
|
buttonDownload?.isEnabled = info.isValid && info.canDownload
|
||||||
buttonRead.isEnabled = info.isValid
|
buttonRead.isEnabled = info.isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNewChaptersChanged(count: Int) {
|
|
||||||
chaptersBadge.counter = count
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBranchPopupMenu(v: View) {
|
private fun showBranchPopupMenu(v: View) {
|
||||||
val menu = PopupMenu(v.context, v)
|
|
||||||
val branches = viewModel.branches.value
|
val branches = viewModel.branches.value
|
||||||
|
if (branches.size <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val menu = PopupMenu(v.context, v)
|
||||||
for ((i, branch) in branches.withIndex()) {
|
for ((i, branch) in branches.withIndex()) {
|
||||||
val title = buildSpannedString {
|
val title = buildSpannedString {
|
||||||
if (branch.isCurrent) {
|
if (branch.isCurrent) {
|
||||||
@@ -600,8 +592,7 @@ class DetailsActivity :
|
|||||||
|
|
||||||
private fun openReader(isIncognitoMode: Boolean) {
|
private fun openReader(isIncognitoMode: Boolean) {
|
||||||
val manga = viewModel.manga.value ?: return
|
val manga = viewModel.manga.value ?: return
|
||||||
val chapterId = viewModel.historyInfo.value.history?.chapterId
|
if (viewModel.historyInfo.value.isChapterMissing) {
|
||||||
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
|
||||||
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
@@ -625,10 +616,7 @@ class DetailsActivity :
|
|||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
tint = tagHighlighter.getTagTint(tag),
|
tint = tagHighlighter.getTagTint(tag),
|
||||||
icon = 0,
|
|
||||||
data = tag,
|
data = tag,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -679,7 +667,8 @@ class DetailsActivity :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val FAV_LABEL_LIMIT = 10
|
private const val FAV_LABEL_LIMIT = 16
|
||||||
|
private const val AUTHOR_LABEL_LIMIT = 16
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga): Intent {
|
fun newIntent(context: Context, manga: Manga): Intent {
|
||||||
return Intent(context, DetailsActivity::class.java)
|
return Intent(context, DetailsActivity::class.java)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.koitharu.kotatsu.browser.BrowserActivity
|
|||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
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.details.ui.pager.ChaptersPagesSheet
|
|
||||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
|
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
|
||||||
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
|
||||||
@@ -135,7 +134,6 @@ class DetailsMenuProvider(
|
|||||||
is DownloadOption.WholeManga -> null
|
is DownloadOption.WholeManga -> null
|
||||||
is DownloadOption.SelectionHint -> {
|
is DownloadOption.SelectionHint -> {
|
||||||
viewModel.startChaptersSelection()
|
viewModel.startChaptersSelection()
|
||||||
ChaptersPagesSheet.show(activity.supportFragmentManager, ChaptersPagesSheet.TAB_CHAPTERS)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user