Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d62ecdc177 | ||
|
|
77cd7dda5f | ||
|
|
bd7099e97c | ||
|
|
b9457a35b9 | ||
|
|
53918ddddd | ||
|
|
84f0da0871 | ||
|
|
11c2e2e3bc | ||
|
|
622c8d1c18 | ||
|
|
10ffae7d4e | ||
|
|
15b48fd902 | ||
|
|
e2d7f2890d | ||
|
|
e01c485949 | ||
|
|
3672c84e8f | ||
|
|
55c5a07c8b | ||
|
|
a3cf32aefb | ||
|
|
c21bf30e91 | ||
|
|
1719547ce0 | ||
|
|
22186825a0 | ||
|
|
b9c83ad5cc | ||
|
|
1359689b23 | ||
|
|
7bad6ad077 | ||
|
|
b9097fa077 | ||
|
|
0b03806ccd | ||
|
|
db9c1279ac | ||
|
|
af510beb7b | ||
|
|
8cf0203b42 | ||
|
|
ea4a81c6ec | ||
|
|
63b53d2244 | ||
|
|
aba6b64074 | ||
|
|
324bfc733b | ||
|
|
fcfb3c9808 | ||
|
|
4ab77064ee | ||
|
|
ca2182d588 | ||
|
|
5ba410acd5 | ||
|
|
06382649c4 | ||
|
|
4f50e905af | ||
|
|
822cdab6ee | ||
|
|
8fad307c9a | ||
|
|
daa545f3db | ||
|
|
56892aea3c | ||
|
|
73e768def0 | ||
|
|
19da2267d6 | ||
|
|
3affec0f88 | ||
|
|
448c688629 | ||
|
|
fc2ab3f795 | ||
|
|
e520e695f9 | ||
|
|
b34f438430 | ||
|
|
72bfe15728 | ||
|
|
60198bc878 | ||
|
|
631c09badb | ||
|
|
2bf6eb6f0e | ||
|
|
1ee8b65ff7 | ||
|
|
d367750331 | ||
|
|
6d1bc5b1fd | ||
|
|
45771adef0 | ||
|
|
d91f613c28 | ||
|
|
988dd767d8 | ||
|
|
d715c175b8 | ||
|
|
a114605be1 | ||
|
|
f7a9e2ef89 | ||
|
|
2aa3133c52 | ||
|
|
2f15ea213d | ||
|
|
19a3f14190 | ||
|
|
fb716d300e | ||
|
|
1fe5095654 | ||
|
|
820d3f2413 | ||
|
|
34903fc951 | ||
|
|
7ec2e0c5cc | ||
|
|
846c346a86 | ||
|
|
f685ed6932 | ||
|
|
98b8ec5c89 | ||
|
|
0e20bf4afe | ||
|
|
fe588c08e2 | ||
|
|
3ee6ac605d | ||
|
|
535feb424c | ||
|
|
2cca696808 | ||
|
|
b5ea0ec7fa | ||
|
|
1e3d2595cf | ||
|
|
960b960726 | ||
|
|
cd29760836 | ||
|
|
27a2883f0a | ||
|
|
326bca2273 | ||
|
|
b32487fcb8 | ||
|
|
105bdff9ab | ||
|
|
6b767523a9 | ||
|
|
396050c051 | ||
|
|
c32d1877ff | ||
|
|
df78d9bf4c | ||
|
|
cc3bea3b2c | ||
|
|
87aa38b4e8 | ||
|
|
ee0215511a | ||
|
|
bd0056394e | ||
|
|
76ea7ab046 | ||
|
|
3d7ea1637f | ||
|
|
4b30905f9c | ||
|
|
bddb8431c5 | ||
|
|
61e02dd827 | ||
|
|
ff4eac8269 | ||
|
|
32eba77639 | ||
|
|
09eb82ca2e | ||
|
|
4d7ff5f6cc | ||
|
|
59dd53c025 | ||
|
|
c98d7561b8 | ||
|
|
5c8157b81f | ||
|
|
7f5ff1ab14 | ||
|
|
018c84b6af | ||
|
|
b95174727a | ||
|
|
0aec2359cf | ||
|
|
62bd5008fd | ||
|
|
89dd7beafe | ||
|
|
cecf3617af |
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 = 633
|
versionCode = 640
|
||||||
versionName = '6.8.3'
|
versionName = '7.0-rc3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -82,18 +82,18 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:44ea9fe709') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:33b00fe65f') {
|
||||||
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.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.8.2'
|
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-ktx:1.7.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.7.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
|
||||||
@@ -101,12 +101,12 @@ dependencies {
|
|||||||
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-rc01'
|
||||||
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-beta01'
|
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.7.0'
|
||||||
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 +121,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'
|
||||||
|
|
||||||
@@ -145,18 +146,19 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
|
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'
|
||||||
|
|||||||
@@ -174,12 +174,12 @@ class TrackerTest {
|
|||||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
|
||||||
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
|
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
|
||||||
repository.syncWithHistory(mangaFull, chapter.id)
|
tracker.syncWithHistory(mangaFull, chapter.id)
|
||||||
|
|
||||||
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
|
||||||
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
|
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
|
||||||
repository.syncWithHistory(mangaFull, chapter.id)
|
tracker.syncWithHistory(mangaFull, chapter.id)
|
||||||
|
|
||||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
|
||||||
|
|||||||
12
app/src/debug/AndroidManifest.xml
Normal file
12
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<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,72 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.text.bold
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.color
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemTrackDebugBinding
|
||||||
|
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
fun trackDebugAD(
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
coil: ImageLoader,
|
||||||
|
clickListener: OnListItemClickListener<TrackDebugItem>,
|
||||||
|
) = adapterDelegateViewBinding<TrackDebugItem, TrackDebugItem, ItemTrackDebugBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemTrackDebugBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
val indicatorNew = ContextCompat.getDrawable(context, R.drawable.ic_new)
|
||||||
|
|
||||||
|
itemView.setOnClickListener { v ->
|
||||||
|
clickListener.onItemClick(item, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
||||||
|
defaultPlaceholders(context)
|
||||||
|
allowRgb565(true)
|
||||||
|
source(item.manga.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
binding.textViewTitle.text = item.manga.title
|
||||||
|
binding.textViewSummary.text = buildSpannedString {
|
||||||
|
item.lastCheckTime?.let {
|
||||||
|
append(
|
||||||
|
DateUtils.getRelativeDateTimeString(
|
||||||
|
context,
|
||||||
|
it.toEpochMilli(),
|
||||||
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
|
DateUtils.WEEK_IN_MILLIS,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.lastResult == TrackEntity.RESULT_FAILED) {
|
||||||
|
append(" - ")
|
||||||
|
bold {
|
||||||
|
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
|
||||||
|
append(getString(R.string.error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.textViewTitle.drawableStart = if (item.newChapters > 0) {
|
||||||
|
indicatorNew
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class TrackDebugItem(
|
||||||
|
val manga: Manga,
|
||||||
|
val lastChapterId: Long,
|
||||||
|
val newChapters: Int,
|
||||||
|
val lastCheckTime: Instant?,
|
||||||
|
val lastChapterDate: Instant?,
|
||||||
|
val lastResult: Int,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is TrackDebugItem && other.manga.id == manga.id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import coil.ImageLoader
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityTrackerDebugBinding
|
||||||
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnListItemClickListener<TrackDebugItem> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
private val viewModel by viewModels<TrackerDebugViewModel>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivityTrackerDebugBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
|
||||||
|
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
|
||||||
|
with(viewBinding.recyclerView) {
|
||||||
|
adapter = tracksAdapter
|
||||||
|
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||||
|
}
|
||||||
|
viewModel.content.observe(this, tracksAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
val rv = viewBinding.recyclerView
|
||||||
|
rv.updatePadding(
|
||||||
|
left = insets.left + rv.paddingTop,
|
||||||
|
right = insets.right + rv.paddingTop,
|
||||||
|
bottom = insets.bottom,
|
||||||
|
)
|
||||||
|
viewBinding.toolbar.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: TrackDebugItem, view: View) {
|
||||||
|
startActivity(DetailsActivity.newIntent(this, item.manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
||||||
|
import org.koitharu.kotatsu.tracker.data.TrackWithManga
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TrackerDebugViewModel @Inject constructor(
|
||||||
|
private val db: MangaDatabase
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val content = db.getTracksDao().observeAll()
|
||||||
|
.map { it.toUiList() }
|
||||||
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
private fun List<TrackWithManga>.toUiList(): List<TrackDebugItem> = map {
|
||||||
|
TrackDebugItem(
|
||||||
|
manga = it.manga.toManga(emptySet()),
|
||||||
|
lastChapterId = it.track.lastChapterId,
|
||||||
|
newChapters = it.track.newChapters,
|
||||||
|
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
|
||||||
|
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
||||||
|
lastResult = it.track.lastResult,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/src/debug/res/layout/activity_tracker_debug.xml
Normal file
44
app/src/debug/res/layout/activity_tracker_debug.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/collapsingToolbarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||||
|
app:toolbarId="@id/toolbar">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_collapseMode="pin" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/list_spacing_normal"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
|
tools:listitem="@layout/item_track_debug" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="72dp"
|
||||||
android:background="@drawable/list_selector"
|
android:background="@drawable/list_selector"
|
||||||
android:clipChildren="false">
|
android:clipChildren="false">
|
||||||
|
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||||
@@ -26,42 +28,29 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/textView_subtitle"
|
app:layout_constraintBottom_toTopOf="@+id/textView_summary"
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_more"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||||
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||||
tools:text="@tools:sample/lorem" />
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView_subtitle"
|
android:id="@+id/textView_summary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_more"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/textView_title"
|
app:layout_constraintTop_toBottomOf="@+id/textView_title"
|
||||||
tools:text="@tools:sample/lorem/random" />
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button_more"
|
|
||||||
style="@style/Widget.Kotatsu.ExploreButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minWidth="120dp"
|
|
||||||
android:text="@string/more"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
@@ -8,4 +8,14 @@
|
|||||||
android:title="@string/leak_canary_display_activity_label"
|
android:title="@string/leak_canary_display_activity_label"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
<item
|
||||||
|
android:id="@id/action_tracker"
|
||||||
|
android:title="@string/check_for_new_chapters"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@id/action_works"
|
||||||
|
android:title="Works"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -94,34 +94,6 @@
|
|||||||
<data android:host="kotatsu.app" />
|
<data android:host="kotatsu.app" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity2"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter android:autoVerify="true">
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:host="kotatsu.app" />
|
|
||||||
<data android:path="/manga" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="kotatsu" />
|
|
||||||
<data android:host="manga" />
|
|
||||||
<data android:host="kotatsu.app" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
@@ -150,7 +122,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"
|
||||||
@@ -281,6 +253,9 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
|||||||
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,22 @@ 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,
|
||||||
|
)
|
||||||
|
tracksDao.delete(oldDetails.id)
|
||||||
|
tracksDao.upsert(newTrack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
progressUpdateUseCase(newManga)
|
progressUpdateUseCase(newManga)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
|||||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
@@ -79,9 +80,7 @@ fun alternativeAD(
|
|||||||
}
|
}
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
||||||
size(CoverSizeResolver(binding.imageViewCover))
|
size(CoverSizeResolver(binding.imageViewCover))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
defaultPlaceholders(context)
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_error_placeholder)
|
|
||||||
transformations(TrimTransformation())
|
transformations(TrimTransformation())
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
tag(item.manga)
|
tag(item.manga)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -30,7 +30,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
|||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class BookmarksFragment :
|
|||||||
val spanSizeLookup = SpanSizeLookup()
|
val spanSizeLookup = SpanSizeLookup()
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
val spanResolver = MangaListSpanResolver(resources)
|
val spanResolver = GridSpanResolver(resources)
|
||||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||||
adapter = bookmarksAdapter
|
adapter = bookmarksAdapter
|
||||||
addOnLayoutChangeListener(spanResolver)
|
addOnLayoutChangeListener(spanResolver)
|
||||||
@@ -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,14 +1,14 @@
|
|||||||
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
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
@@ -30,9 +30,7 @@ fun bookmarkLargeAD(
|
|||||||
bind {
|
bind {
|
||||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||||
size(CoverSizeResolver(binding.imageViewThumb))
|
size(CoverSizeResolver(binding.imageViewThumb))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
defaultPlaceholders(context)
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_error_placeholder)
|
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
tag(item)
|
tag(item)
|
||||||
decodeRegion(item.scroll)
|
decodeRegion(item.scroll)
|
||||||
@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.bookmarks.ui.adapter
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
@@ -29,9 +29,7 @@ fun bookmarkListAD(
|
|||||||
bind {
|
bind {
|
||||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||||
size(CoverSizeResolver(binding.imageViewThumb))
|
size(CoverSizeResolver(binding.imageViewThumb))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
defaultPlaceholders(context)
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_error_placeholder)
|
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
tag(item)
|
tag(item)
|
||||||
decodeRegion(item.scroll)
|
decodeRegion(item.scroll)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import coil.ImageLoader
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
|
||||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
|
||||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.plus
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class BookmarksSheet :
|
|
||||||
BaseAdaptiveSheet<SheetPagesBinding>(),
|
|
||||||
AdaptiveSheetCallback,
|
|
||||||
OnListItemClickListener<Bookmark> {
|
|
||||||
|
|
||||||
private val viewModel by viewModels<BookmarksSheetViewModel>()
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var settings: AppSettings
|
|
||||||
|
|
||||||
private var bookmarksAdapter: BookmarksAdapter? = null
|
|
||||||
private var spanResolver: MangaListSpanResolver? = null
|
|
||||||
|
|
||||||
private val spanSizeLookup = SpanSizeLookup()
|
|
||||||
private val listCommitCallback = Runnable {
|
|
||||||
spanSizeLookup.invalidateCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
|
||||||
return SheetPagesBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
|
||||||
addSheetCallback(this)
|
|
||||||
spanResolver = MangaListSpanResolver(binding.root.resources)
|
|
||||||
bookmarksAdapter = BookmarksAdapter(
|
|
||||||
coil = coil,
|
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
|
||||||
clickListener = this@BookmarksSheet,
|
|
||||||
headerClickListener = null,
|
|
||||||
)
|
|
||||||
viewBinding?.headerBar?.setTitle(R.string.bookmarks)
|
|
||||||
with(binding.recyclerView) {
|
|
||||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
|
||||||
adapter = bookmarksAdapter
|
|
||||||
addOnLayoutChangeListener(spanResolver)
|
|
||||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
|
||||||
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
|
|
||||||
}
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onThumbnailsChanged)
|
|
||||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
spanResolver = null
|
|
||||||
bookmarksAdapter = null
|
|
||||||
spanSizeLookup.invalidateCache()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: Bookmark, view: View) {
|
|
||||||
val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener)
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onPageSelected(ReaderPage(item.toMangaPage(), item.page, item.chapterId))
|
|
||||||
} else {
|
|
||||||
val intent = IntentBuilder(view.context)
|
|
||||||
.manga(viewModel.manga)
|
|
||||||
.bookmark(item)
|
|
||||||
.incognito(true)
|
|
||||||
.build()
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChanged(sheet: View, newState: Int) {
|
|
||||||
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onThumbnailsChanged(list: List<ListModel>) {
|
|
||||||
val adapter = bookmarksAdapter ?: return
|
|
||||||
if (adapter.itemCount == 0) {
|
|
||||||
var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent }
|
|
||||||
if (position > 0) {
|
|
||||||
val spanCount = spanResolver?.spanCount ?: 0
|
|
||||||
val offset = if (position > spanCount + 1) {
|
|
||||||
(resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt()
|
|
||||||
} else {
|
|
||||||
position = 0
|
|
||||||
0
|
|
||||||
}
|
|
||||||
val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset)
|
|
||||||
adapter.setItems(list, listCommitCallback + scrollCallback)
|
|
||||||
} else {
|
|
||||||
adapter.setItems(list, listCommitCallback)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
adapter.setItems(list, listCommitCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
isSpanIndexCacheEnabled = true
|
|
||||||
isSpanGroupIndexCacheEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSpanSize(position: Int): Int {
|
|
||||||
val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
|
||||||
return when (bookmarksAdapter?.getItemViewType(position)) {
|
|
||||||
ListItemType.PAGE_THUMB.ordinal -> 1
|
|
||||||
else -> total
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateCache() {
|
|
||||||
invalidateSpanGroupIndexCache()
|
|
||||||
invalidateSpanIndexCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val ARG_MANGA = "manga"
|
|
||||||
|
|
||||||
private const val TAG = "BookmarksSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager, manga: Manga) {
|
|
||||||
BookmarksSheet().withArgs(1) {
|
|
||||||
putParcelable(ARG_MANGA, ParcelableManga(manga))
|
|
||||||
}.showDistinct(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
|
||||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
@HiltViewModel
|
|
||||||
class BookmarksSheetViewModel @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
bookmarksRepository: BookmarksRepository,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val manga = savedStateHandle.require<ParcelableManga>(BookmarksSheet.ARG_MANGA).manga
|
|
||||||
private val chaptersLazy = SuspendLazy {
|
|
||||||
requireNotNull(manga.chapters ?: mangaRepositoryFactory.create(manga.source).getDetails(manga).chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
val content: StateFlow<List<ListModel>> = bookmarksRepository.observeBookmarks(manga)
|
|
||||||
.map { mapList(it) }
|
|
||||||
.withErrorHandling()
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingFooter()))
|
|
||||||
|
|
||||||
private suspend fun mapList(bookmarks: List<Bookmark>): List<ListModel> {
|
|
||||||
val chapters = chaptersLazy.get()
|
|
||||||
val bookmarksMap = bookmarks.groupBy { it.chapterId }
|
|
||||||
val result = ArrayList<ListModel>(bookmarks.size + bookmarksMap.size)
|
|
||||||
for (chapter in chapters) {
|
|
||||||
val b = bookmarksMap[chapter.id]
|
|
||||||
if (b.isNullOrEmpty()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += ListHeader(chapter.name)
|
|
||||||
result.addAll(b)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,13 +36,14 @@ 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)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setGroup(GROUP_CAPTCHA)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setVisibility(
|
.setVisibility(
|
||||||
if (exception.source?.contentType == ContentType.HENTAI) {
|
if (exception.source?.contentType == ContentType.HENTAI) {
|
||||||
@@ -55,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) {
|
||||||
@@ -82,5 +99,7 @@ class CaptchaNotifier(
|
|||||||
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
|
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
|
||||||
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 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,9 +26,6 @@ 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.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
@@ -40,9 +37,10 @@ import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
|||||||
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.IncognitoModeIndicator
|
|
||||||
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.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
|
||||||
@@ -50,11 +48,11 @@ 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.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
|
||||||
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
|
||||||
@@ -87,7 +85,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,
|
||||||
@@ -99,11 +97,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)
|
||||||
@@ -113,7 +114,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,27 +149,13 @@ interface AppModule {
|
|||||||
fun provideActivityLifecycleCallbacks(
|
fun provideActivityLifecycleCallbacks(
|
||||||
appProtectHelper: AppProtectHelper,
|
appProtectHelper: AppProtectHelper,
|
||||||
activityRecreationHandle: ActivityRecreationHandle,
|
activityRecreationHandle: ActivityRecreationHandle,
|
||||||
incognitoModeIndicator: IncognitoModeIndicator,
|
|
||||||
acraScreenLogger: AcraScreenLogger,
|
acraScreenLogger: AcraScreenLogger,
|
||||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||||
appProtectHelper,
|
appProtectHelper,
|
||||||
activityRecreationHandle,
|
activityRecreationHandle,
|
||||||
incognitoModeIndicator,
|
|
||||||
acraScreenLogger,
|
acraScreenLogger,
|
||||||
)
|
)
|
||||||
|
|
||||||
@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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
application.registerComponentCallbacks(this)
|
application.registerComponentCallbacks(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
||||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
private val pagesCache =
|
||||||
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
|
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)
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = true
|
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
|
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,9 @@ abstract class MangaDao {
|
|||||||
@Query("SELECT * FROM manga WHERE source = :source")
|
@Query("SELECT * FROM manga WHERE source = :source")
|
||||||
abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
|
abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
|
||||||
|
|
||||||
|
@Query("SELECT author FROM manga WHERE author LIKE :query GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
|
||||||
|
abstract suspend fun findAuthors(query: String, limit: Int): List<String>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||||
@@ -37,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
|
||||||
|
|||||||
@@ -20,11 +20,8 @@ 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")
|
|
||||||
abstract fun observeDisabled(): Flow<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>>
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ interface TrackLogsDao {
|
|||||||
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
|
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
|
||||||
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
|
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
|
||||||
|
fun observeUnreadCount(): Flow<Int>
|
||||||
|
|
||||||
@Query("DELETE FROM track_logs")
|
@Query("DELETE FROM track_logs")
|
||||||
suspend fun clear()
|
suspend fun clear()
|
||||||
|
|
||||||
|
@Query("UPDATE track_logs SET unread = 0 WHERE id = :id")
|
||||||
|
suspend fun markAsRead(id: Long)
|
||||||
|
|
||||||
@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()
|
||||||
|
|
||||||
|
|||||||
@@ -12,5 +12,7 @@ class Migration19To20 : Migration(19, 20) {
|
|||||||
db.execSQL("CREATE TABLE tracks (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
db.execSQL("CREATE TABLE tracks (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||||
db.execSQL("INSERT INTO tracks SELECT manga_id, last_chapter_id, chapters_new, last_check AS last_check_time, 0 AS last_chapter_date, 0 AS last_result FROM tracks_bk")
|
db.execSQL("INSERT INTO tracks SELECT manga_id, last_chapter_id, chapters_new, last_check AS last_check_time, 0 AS last_chapter_date, 0 AS last_result FROM tracks_bk")
|
||||||
db.execSQL("DROP TABLE tracks_bk")
|
db.execSQL("DROP TABLE tracks_bk")
|
||||||
|
|
||||||
|
db.execSQL("ALTER TABLE track_logs ADD COLUMN `unread` INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okio.IOException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class CloudFlareBlockedException(
|
||||||
|
val url: String,
|
||||||
|
val source: MangaSource?,
|
||||||
|
) : IOException("Blocked by CloudFlare")
|
||||||
@@ -6,7 +6,6 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import okhttp3.Headers
|
|
||||||
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
|
||||||
@@ -30,7 +29,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
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>
|
||||||
|
|
||||||
constructor(activity: FragmentActivity) {
|
constructor(activity: FragmentActivity) {
|
||||||
this.activity = activity
|
this.activity = activity
|
||||||
@@ -55,7 +54,7 @@ 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 NotFoundException -> {
|
is NotFoundException -> {
|
||||||
openInBrowser(e.url)
|
openInBrowser(e.url)
|
||||||
@@ -70,9 +69,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 ->
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
||||||
@@ -17,14 +18,23 @@ class CloudFlareInterceptor : Interceptor {
|
|||||||
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
|
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
|
||||||
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
|
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
|
||||||
} ?: return response
|
} ?: return response
|
||||||
if (content.getElementById("challenge-error-title") != null) {
|
val hasCaptcha = content.getElementById("challenge-error-title") != null
|
||||||
|
val isBlocked = content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null
|
||||||
|
if (hasCaptcha || isBlocked) {
|
||||||
val request = response.request
|
val request = response.request
|
||||||
response.closeQuietly()
|
response.closeQuietly()
|
||||||
throw CloudFlareProtectedException(
|
if (isBlocked) {
|
||||||
url = request.url.toString(),
|
throw CloudFlareBlockedException(
|
||||||
source = request.tag(MangaSource::class.java),
|
url = request.url.toString(),
|
||||||
headers = request.headers,
|
source = request.tag(MangaSource::class.java),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
throw CloudFlareProtectedException(
|
||||||
|
url = request.url.toString(),
|
||||||
|
source = request.tag(MangaSource::class.java),
|
||||||
|
headers = request.headers,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ 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.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
|
||||||
@@ -50,10 +52,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 +66,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()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|||||||
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 +38,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 +50,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)
|
||||||
@@ -86,4 +75,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)
|
||||||
|
|||||||
@@ -170,10 +170,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()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet
|
|||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -74,6 +75,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isNavLabelsVisible: Boolean
|
val isNavLabelsVisible: Boolean
|
||||||
get() = prefs.getBoolean(KEY_NAV_LABELS, true)
|
get() = prefs.getBoolean(KEY_NAV_LABELS, true)
|
||||||
|
|
||||||
|
val isNavBarPinned: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_NAV_PINNED, false)
|
||||||
|
|
||||||
var gridSize: Int
|
var gridSize: Int
|
||||||
get() = prefs.getInt(KEY_GRID_SIZE, 100)
|
get() = prefs.getInt(KEY_GRID_SIZE, 100)
|
||||||
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
|
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
|
||||||
@@ -142,6 +146,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isTrackerWifiOnly: Boolean
|
val isTrackerWifiOnly: Boolean
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
|
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
|
||||||
|
|
||||||
|
val trackerFrequencyFactor: Float
|
||||||
|
get() = prefs.getString(KEY_TRACKER_FREQUENCY, null)?.toFloatOrNull() ?: 1f
|
||||||
|
|
||||||
val isTrackerNotificationsEnabled: Boolean
|
val isTrackerNotificationsEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
||||||
|
|
||||||
@@ -172,6 +179,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
|
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
|
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
|
||||||
|
|
||||||
|
var isUpdatedGroupingEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_UPDATED_GROUPING, true)
|
||||||
|
set(value) = prefs.edit { putBoolean(KEY_UPDATED_GROUPING, value) }
|
||||||
|
|
||||||
|
var isFeedHeaderVisible: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
|
||||||
|
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
|
||||||
|
|
||||||
val isReadingIndicatorsEnabled: Boolean
|
val isReadingIndicatorsEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
|
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
|
||||||
|
|
||||||
@@ -206,6 +221,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
|
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
|
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
|
||||||
|
|
||||||
|
val searchSuggestionTypes: Set<SearchSuggestionType>
|
||||||
|
get() = prefs.getStringSet(KEY_SEARCH_SUGGESTION_TYPES, null)?.let { stringSet ->
|
||||||
|
stringSet.mapNotNullTo(EnumSet.noneOf(SearchSuggestionType::class.java)) { x ->
|
||||||
|
enumValueOf<SearchSuggestionType>(x)
|
||||||
|
}
|
||||||
|
} ?: EnumSet.allOf(SearchSuggestionType::class.java)
|
||||||
|
|
||||||
val isLoggingEnabled: Boolean
|
val isLoggingEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
|
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
|
||||||
|
|
||||||
@@ -230,11 +252,20 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
|
|
||||||
val defaultDetailsTab: Int
|
val defaultDetailsTab: Int
|
||||||
get() = if (isPagesTabEnabled) {
|
get() = if (isPagesTabEnabled) {
|
||||||
prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull()?.coerceIn(0, 1) ?: 0
|
val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: -1
|
||||||
|
if (raw == -1) {
|
||||||
|
lastDetailsTab
|
||||||
|
} else {
|
||||||
|
raw
|
||||||
|
}.coerceIn(0, 2)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastDetailsTab: Int
|
||||||
|
get() = prefs.getInt(KEY_DETAILS_LAST_TAB, 0)
|
||||||
|
set(value) = prefs.edit { putInt(KEY_DETAILS_LAST_TAB, value) }
|
||||||
|
|
||||||
val isContentPrefetchEnabled: Boolean
|
val isContentPrefetchEnabled: Boolean
|
||||||
get() {
|
get() {
|
||||||
if (isBackgroundNetworkRestricted()) {
|
if (isBackgroundNetworkRestricted()) {
|
||||||
@@ -250,7 +281,7 @@ 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
|
val isNewSourcesTipEnabled: Boolean
|
||||||
@@ -388,9 +419,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isRelatedMangaEnabled: Boolean
|
val isRelatedMangaEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
|
get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
|
||||||
|
|
||||||
val isWebtoonZoomEnable: Boolean
|
val isWebtoonZoomEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
||||||
|
|
||||||
|
var isWebtoonGapsEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
|
||||||
|
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
|
||||||
|
|
||||||
@get:FloatRange(from = 0.0, to = 0.5)
|
@get:FloatRange(from = 0.0, to = 0.5)
|
||||||
val defaultWebtoonZoomOut: Float
|
val defaultWebtoonZoomOut: Float
|
||||||
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
|
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
|
||||||
@@ -541,6 +576,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons"
|
const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons"
|
||||||
const val KEY_TRACKER_ENABLED = "tracker_enabled"
|
const val KEY_TRACKER_ENABLED = "tracker_enabled"
|
||||||
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
|
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
|
||||||
|
const val KEY_TRACKER_FREQUENCY = "tracker_freq"
|
||||||
const val KEY_TRACK_SOURCES = "track_sources"
|
const val KEY_TRACK_SOURCES = "track_sources"
|
||||||
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"
|
||||||
@@ -566,6 +602,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
|
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
|
||||||
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
|
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
|
||||||
const val KEY_HISTORY_GROUPING = "history_grouping"
|
const val KEY_HISTORY_GROUPING = "history_grouping"
|
||||||
|
const val KEY_UPDATED_GROUPING = "updated_grouping"
|
||||||
const val KEY_READING_INDICATORS = "reading_indicators"
|
const val KEY_READING_INDICATORS = "reading_indicators"
|
||||||
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
||||||
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
|
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
|
||||||
@@ -600,6 +637,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||||
const val KEY_HISTORY_ORDER = "history_order"
|
const val KEY_HISTORY_ORDER = "history_order"
|
||||||
const val KEY_FAVORITES_ORDER = "fav_order"
|
const val KEY_FAVORITES_ORDER = "fav_order"
|
||||||
|
const val KEY_WEBTOON_GAPS = "webtoon_gaps"
|
||||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||||
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
|
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
|
||||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||||
@@ -626,6 +664,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_RELATED_MANGA = "related_manga"
|
const val KEY_RELATED_MANGA = "related_manga"
|
||||||
const val KEY_NAV_MAIN = "nav_main"
|
const val KEY_NAV_MAIN = "nav_main"
|
||||||
const val KEY_NAV_LABELS = "nav_labels"
|
const val KEY_NAV_LABELS = "nav_labels"
|
||||||
|
const val KEY_NAV_PINNED = "nav_pinned"
|
||||||
const val KEY_32BIT_COLOR = "enhanced_colors"
|
const val KEY_32BIT_COLOR = "enhanced_colors"
|
||||||
const val KEY_SOURCES_ORDER = "sources_sort_order"
|
const val KEY_SOURCES_ORDER = "sources_sort_order"
|
||||||
const val KEY_SOURCES_CATALOG = "sources_catalog"
|
const val KEY_SOURCES_CATALOG = "sources_catalog"
|
||||||
@@ -636,11 +675,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_IGNORE_DOZE = "ignore_dose"
|
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_READING_TIME = "reading_time"
|
const val KEY_READING_TIME = "reading_time"
|
||||||
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_UPDATE = "app_update"
|
||||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||||
|
const val KEY_FEED_HEADER = "feed_header"
|
||||||
|
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ enum class NavItem(
|
|||||||
EXPLORE(R.id.nav_explore, R.string.explore, R.drawable.ic_explore_selector),
|
EXPLORE(R.id.nav_explore, R.string.explore, R.drawable.ic_explore_selector),
|
||||||
SUGGESTIONS(R.id.nav_suggestions, R.string.suggestions, R.drawable.ic_suggestion_selector),
|
SUGGESTIONS(R.id.nav_suggestions, R.string.suggestions, R.drawable.ic_suggestion_selector),
|
||||||
FEED(R.id.nav_feed, R.string.feed, R.drawable.ic_feed_selector),
|
FEED(R.id.nav_feed, R.string.feed, R.drawable.ic_feed_selector),
|
||||||
|
UPDATED(R.id.nav_updated, R.string.updated, R.drawable.ic_updated_selector),
|
||||||
BOOKMARKS(R.id.nav_bookmarks, R.string.bookmarks, R.drawable.ic_bookmark_selector),
|
BOOKMARKS(R.id.nav_bookmarks, R.string.bookmarks, R.drawable.ic_bookmark_selector),
|
||||||
;
|
;
|
||||||
|
|
||||||
fun isAvailable(settings: AppSettings): Boolean = when (this) {
|
fun isAvailable(settings: AppSettings): Boolean = when (this) {
|
||||||
SUGGESTIONS -> settings.isSuggestionsEnabled
|
SUGGESTIONS -> settings.isSuggestionsEnabled
|
||||||
FEED -> settings.isTrackerEnabled
|
UPDATED, FEED -> settings.isTrackerEnabled
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
enum class SearchSuggestionType(
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
|
GENRES(R.string.genres),
|
||||||
|
QUERIES_RECENT(R.string.recent_queries),
|
||||||
|
QUERIES_SUGGEST(R.string.suggested_queries),
|
||||||
|
MANGA(R.string.content_type_manga),
|
||||||
|
SOURCES(R.string.remote_sources),
|
||||||
|
AUTHORS(R.string.authors),
|
||||||
|
}
|
||||||
@@ -3,33 +3,27 @@ package org.koitharu.kotatsu.core.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
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 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
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
@@ -72,7 +66,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
onBackPressedDispatcher.addCallback(actionModeDelegate)
|
onBackPressedDispatcher.addCallback(actionModeDelegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
putDataToExtras(intent)
|
putDataToExtras(intent)
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
}
|
}
|
||||||
@@ -98,6 +92,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
|
supportFragmentManager.popBackStack()
|
||||||
|
return false
|
||||||
|
}
|
||||||
dispatchNavigateUp()
|
dispatchNavigateUp()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -123,32 +121,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() {
|
||||||
@@ -181,6 +160,12 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
|
|||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
|
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
|
||||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
@@ -48,4 +53,14 @@ open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun observeItems(): Flow<List<T>> = callbackFlow {
|
||||||
|
val listListener = ListListener<T> { _, list ->
|
||||||
|
trySendBlocking(list)
|
||||||
|
}
|
||||||
|
addListListener(listListener)
|
||||||
|
awaitClose { removeListListener(listListener) }
|
||||||
|
}.onStart {
|
||||||
|
emit(items)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
package org.koitharu.kotatsu.core.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.annotation.AnyThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
@@ -39,8 +41,10 @@ abstract class CoroutineIntentService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
|
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
protected abstract fun onError(startId: Int, error: Throwable)
|
protected abstract fun onError(startId: Int, error: Throwable)
|
||||||
|
|
||||||
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
|
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.image
|
||||||
|
|
||||||
|
import android.animation.TimeAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import kotlin.math.abs
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
|
||||||
|
|
||||||
|
private val colorLow = context.getThemeColor(materialR.attr.colorSurfaceContainerLowest)
|
||||||
|
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainerHighest)
|
||||||
|
private var currentColor: Int = colorLow
|
||||||
|
private val interpolator = FastOutSlowInInterpolator()
|
||||||
|
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||||
|
private val timeAnimator = TimeAnimator()
|
||||||
|
|
||||||
|
init {
|
||||||
|
timeAnimator.setTimeListener(this)
|
||||||
|
updateColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
if (!isRunning && period > 0) {
|
||||||
|
updateColor()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
canvas.drawColor(currentColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) {
|
||||||
|
// this.alpha = alpha FIXME coil's crossfade
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||||
|
|
||||||
|
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||||
|
|
||||||
|
override fun getAlpha(): Int = 255
|
||||||
|
|
||||||
|
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||||
|
callback?.also {
|
||||||
|
updateColor()
|
||||||
|
it.invalidateDrawable(this)
|
||||||
|
} ?: stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
timeAnimator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
timeAnimator.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||||
|
|
||||||
|
private fun updateColor() {
|
||||||
|
if (period <= 0f) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val ph = period / 2
|
||||||
|
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||||
|
currentColor = ArgbEvaluatorCompat.getInstance()
|
||||||
|
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class FitHeightGridLayoutManager : GridLayoutManager {
|
|||||||
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
||||||
val parentBottom = height - paddingBottom
|
val parentBottom = height - paddingBottom
|
||||||
val offset = parentBottom - bottom
|
val offset = parentBottom - bottom
|
||||||
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
|
super.layoutDecoratedWithMargins(child, left, top, right, bottom + offset)
|
||||||
} else {
|
} else {
|
||||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class FitHeightLinearLayoutManager : LinearLayoutManager {
|
|||||||
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
||||||
val parentBottom = height - paddingBottom
|
val parentBottom = height - paddingBottom
|
||||||
val offset = parentBottom - bottom
|
val offset = parentBottom - bottom
|
||||||
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
|
super.layoutDecoratedWithMargins(child, left, top, right, bottom + offset)
|
||||||
} else {
|
} else {
|
||||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.list
|
package org.koitharu.kotatsu.core.ui.list
|
||||||
|
|
||||||
|
import android.app.Notification.Action
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -80,8 +81,7 @@ class ListSelectionController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onItemLongClick(id: Long): Boolean {
|
fun onItemLongClick(id: Long): Boolean {
|
||||||
startActionMode()
|
return startActionMode()?.also {
|
||||||
return actionMode?.also {
|
|
||||||
decoration.setItemIsChecked(id, true)
|
decoration.setItemIsChecked(id, true)
|
||||||
notifySelectionChanged()
|
notifySelectionChanged()
|
||||||
} != null
|
} != null
|
||||||
@@ -105,9 +105,9 @@ class ListSelectionController(
|
|||||||
actionMode = null
|
actionMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startActionMode() {
|
private fun startActionMode(): ActionMode? {
|
||||||
if (actionMode == null) {
|
return actionMode ?: appCompatDelegate.startSupportActionMode(this).also {
|
||||||
actionMode = appCompatDelegate.startSupportActionMode(this)
|
actionMode = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +2,10 @@ package org.koitharu.kotatsu.core.ui.sheet
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewParent
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.ancestors
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
@@ -109,7 +112,16 @@ sealed class AdaptiveSheetBehavior {
|
|||||||
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
|
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
|
||||||
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
|
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
fun from(fragment: DialogFragment): AdaptiveSheetBehavior? {
|
||||||
|
from(fragment.dialog)?.let { return it }
|
||||||
|
val rootView = fragment.view ?: return null
|
||||||
|
for (parent in rootView.ancestors) {
|
||||||
|
from(parent)?.let { return it }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
||||||
is BottomSheetDialog -> Bottom(dialog.behavior)
|
is BottomSheetDialog -> Bottom(dialog.behavior)
|
||||||
is SideSheetDialog -> Side(dialog.behavior)
|
is SideSheetDialog -> Side(dialog.behavior)
|
||||||
else -> null
|
else -> null
|
||||||
@@ -121,5 +133,10 @@ sealed class AdaptiveSheetBehavior {
|
|||||||
is SideSheetBehavior<*> -> Side(behavior)
|
is SideSheetBehavior<*> -> Side(behavior)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun from(parent: ViewParent): AdaptiveSheetBehavior? {
|
||||||
|
val lp = ((parent as? View)?.layoutParams as? CoordinatorLayout.LayoutParams) ?: return null
|
||||||
|
return from(lp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,38 +2,34 @@ 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
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.LayoutParams
|
import android.view.ViewGroup.LayoutParams
|
||||||
|
import androidx.activity.ComponentDialog
|
||||||
import androidx.activity.OnBackPressedDispatcher
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
|
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.LifecycleOwner
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.sidesheet.SideSheetDialog
|
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.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
|
||||||
@@ -43,16 +39,16 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
get() = requireViewBinding()
|
get() = requireViewBinding()
|
||||||
|
|
||||||
protected val behavior: AdaptiveSheetBehavior?
|
protected val behavior: AdaptiveSheetBehavior?
|
||||||
get() = AdaptiveSheetBehavior.from(dialog)
|
get() = AdaptiveSheetBehavior.from(this)
|
||||||
|
|
||||||
@JvmField
|
var actionModeDelegate: ActionModeDelegate? = null
|
||||||
val actionModeDelegate = ActionModeDelegate()
|
private set
|
||||||
|
|
||||||
val isExpanded: Boolean
|
val isExpanded: Boolean
|
||||||
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||||
|
|
||||||
val onBackPressedDispatcher: OnBackPressedDispatcher
|
val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||||
get() = requireComponentDialog().onBackPressedDispatcher
|
get() = (dialog as? ComponentDialog)?.onBackPressedDispatcher ?: requireActivity().onBackPressedDispatcher
|
||||||
|
|
||||||
var isLocked = false
|
var isLocked = false
|
||||||
private set
|
private set
|
||||||
@@ -71,11 +67,15 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val binding = requireViewBinding()
|
val binding = requireViewBinding()
|
||||||
|
if (actionModeDelegate == null) {
|
||||||
|
actionModeDelegate = (activity as? BaseActivity<*>)?.actionModeDelegate
|
||||||
|
}
|
||||||
onViewBindingCreated(binding, savedInstanceState)
|
onViewBindingCreated(binding, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
viewBinding = null
|
viewBinding = null
|
||||||
|
actionModeDelegate = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,50 +86,33 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
} else {
|
} else {
|
||||||
BottomSheetDialogImpl(context, theme)
|
BottomSheetDialogImpl(context, theme)
|
||||||
}
|
}
|
||||||
dialog.onBackPressedDispatcher.addCallback(actionModeDelegate)
|
actionModeDelegate = ActionModeDelegate().also {
|
||||||
|
dialog.onBackPressedDispatcher.addCallback(it)
|
||||||
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
||||||
val b = behavior ?: return
|
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
|
||||||
if (rootView != null) {
|
if (rootView != null) {
|
||||||
callback.onStateChanged(rootView, b.state)
|
callback.onStateChanged(rootView, b.state)
|
||||||
}
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(CallbackRemoveObserver(b, callback))
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
@@ -137,8 +120,9 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
||||||
|
|
||||||
fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? {
|
fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? {
|
||||||
val appCompatDialog = dialog as? AppCompatDialog ?: return null
|
val delegate =
|
||||||
return appCompatDialog.delegate.startSupportActionMode(callback)
|
(dialog as? AppCompatDialog)?.delegate ?: (activity as? AppCompatActivity)?.delegate ?: return null
|
||||||
|
return delegate.startSupportActionMode(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
||||||
@@ -283,4 +267,16 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CallbackRemoveObserver(
|
||||||
|
private val behavior: AdaptiveSheetBehavior,
|
||||||
|
private val callback: AdaptiveSheetCallback,
|
||||||
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
super.onDestroy(owner)
|
||||||
|
owner.lifecycle.removeObserver(this)
|
||||||
|
behavior.removeCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class BottomSheetCollapseCallback(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.content.withStyledAttributes
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.measureDimension
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class DotsIndicator @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = R.attr.dotIndicatorStyle,
|
||||||
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private var indicatorSize = context.resources.resolveDp(12f)
|
||||||
|
private var dotSpacing = 0f
|
||||||
|
private var smallDotScale = 0.33f
|
||||||
|
private var smallDotAlpha = 0.6f
|
||||||
|
private var positionOffset: Float = 0f
|
||||||
|
private var position: Int = 0
|
||||||
|
private val inset = context.resources.resolveDp(1f)
|
||||||
|
|
||||||
|
var max: Int = 6
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
requestLayout()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var progress: Int
|
||||||
|
get() = position
|
||||||
|
set(value) {
|
||||||
|
if (position != value) {
|
||||||
|
position = value
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
context.withStyledAttributes(attrs, R.styleable.DotsIndicator, defStyleAttr) {
|
||||||
|
paint.color = getColor(
|
||||||
|
R.styleable.DotsIndicator_dotColor,
|
||||||
|
context.getThemeColor(materialR.attr.colorOnBackground, Color.DKGRAY),
|
||||||
|
)
|
||||||
|
indicatorSize = getDimension(R.styleable.DotsIndicator_dotSize, indicatorSize)
|
||||||
|
dotSpacing = getDimension(R.styleable.DotsIndicator_dotSpacing, dotSpacing)
|
||||||
|
smallDotScale = getFloat(R.styleable.DotsIndicator_dotScale, smallDotScale).coerceIn(0f, 1f)
|
||||||
|
smallDotAlpha = getFloat(R.styleable.DotsIndicator_dotAlpha, smallDotAlpha).coerceIn(0f, 1f)
|
||||||
|
max = getInt(R.styleable.DotsIndicator_android_max, max)
|
||||||
|
position = getInt(R.styleable.DotsIndicator_android_progress, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
val dotSize = getDotSize()
|
||||||
|
val y = paddingTop + (height - paddingTop - paddingBottom) / 2f
|
||||||
|
var x = paddingLeft + dotSize / 2f
|
||||||
|
val radius = dotSize / 2f - inset
|
||||||
|
val spacing = (width - paddingLeft - paddingRight) / max.toFloat() - dotSize
|
||||||
|
x += spacing / 2f
|
||||||
|
for (i in 0 until max) {
|
||||||
|
val scale = when (i) {
|
||||||
|
position -> (1f - smallDotScale) * (1f - positionOffset) + smallDotScale
|
||||||
|
position + 1 -> (1f - smallDotScale) * positionOffset + smallDotScale
|
||||||
|
else -> smallDotScale
|
||||||
|
}
|
||||||
|
paint.alpha = (255 * when (i) {
|
||||||
|
position -> (1f - smallDotAlpha) * (1f - positionOffset) + smallDotAlpha
|
||||||
|
position + 1 -> (1f - smallDotAlpha) * positionOffset + smallDotAlpha
|
||||||
|
else -> smallDotAlpha
|
||||||
|
}).toInt()
|
||||||
|
canvas.drawCircle(x, y, radius * scale, paint)
|
||||||
|
x += spacing + dotSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val dotSize = getDotSize()
|
||||||
|
val desiredHeight = (dotSize + paddingTop + paddingBottom).toIntUp()
|
||||||
|
val desiredWidth = ((dotSize + dotSpacing) * max).toIntUp() + paddingLeft + paddingRight
|
||||||
|
setMeasuredDimension(
|
||||||
|
measureDimension(desiredWidth, widthMeasureSpec),
|
||||||
|
measureDimension(desiredHeight, heightMeasureSpec),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindToViewPager(pager: ViewPager2) {
|
||||||
|
pager.registerOnPageChangeCallback(ViewPagerCallback())
|
||||||
|
pager.adapter?.let {
|
||||||
|
it.registerAdapterDataObserver(AdapterObserver(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDotSize() = if (indicatorSize <= 0) {
|
||||||
|
(height - paddingTop - paddingBottom).toFloat()
|
||||||
|
} else {
|
||||||
|
indicatorSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ViewPagerCallback : ViewPager2.OnPageChangeCallback() {
|
||||||
|
|
||||||
|
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||||
|
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||||
|
this@DotsIndicator.position = position
|
||||||
|
this@DotsIndicator.positionOffset = positionOffset
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class AdapterObserver(
|
||||||
|
private val adapter: RecyclerView.Adapter<*>,
|
||||||
|
) : AdapterDataObserver() {
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
super.onChanged()
|
||||||
|
max = adapter.itemCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
super.onItemRangeInserted(positionStart, itemCount)
|
||||||
|
max = adapter.itemCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
|
super.onItemRangeRemoved(positionStart, itemCount)
|
||||||
|
max = adapter.itemCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,15 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var dyRatio = 1F
|
private var dyRatio = 1F
|
||||||
|
|
||||||
|
var isPinned: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
offsetAnimator = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
||||||
return dependency is AppBarLayout
|
return dependency is AppBarLayout
|
||||||
}
|
}
|
||||||
@@ -51,7 +60,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
axes: Int,
|
axes: Int,
|
||||||
type: Int,
|
type: Int,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
if (isPinned || axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
lastStartedType = type
|
lastStartedType = type
|
||||||
@@ -69,7 +78,9 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
type: Int,
|
type: Int,
|
||||||
) {
|
) {
|
||||||
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
||||||
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
if (!isPinned) {
|
||||||
|
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopNestedScroll(
|
override fun onStopNestedScroll(
|
||||||
@@ -78,7 +89,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
target: View,
|
target: View,
|
||||||
type: Int,
|
type: Int,
|
||||||
) {
|
) {
|
||||||
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
if (!isPinned && (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH)) {
|
||||||
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
|
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package org.koitharu.kotatsu.core.ui.widgets
|
|||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Outline
|
import android.graphics.Outline
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@@ -14,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.getThemeColor
|
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
|
||||||
@@ -35,11 +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 colorBase = context.getThemeColor(materialR.attr.colorPrimaryContainer)
|
private var targetProgress = 0f
|
||||||
private var colorProgress = context.getThemeColor(materialR.attr.colorPrimary)
|
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||||
private var colorText = context.getThemeColor(materialR.attr.colorOnPrimaryContainer)
|
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) {
|
||||||
@@ -69,11 +76,14 @@ class ProgressButton @JvmOverloads constructor(
|
|||||||
)
|
)
|
||||||
textViewTitle.text = getText(R.styleable.ProgressButton_title)
|
textViewTitle.text = getText(R.styleable.ProgressButton_title)
|
||||||
textViewSubtitle.text = getText(R.styleable.ProgressButton_subtitle)
|
textViewSubtitle.text = getText(R.styleable.ProgressButton_subtitle)
|
||||||
colorBase = getColor(R.styleable.ProgressButton_baseColor, colorBase)
|
colorBase = getColorStateList(R.styleable.ProgressButton_baseColor)
|
||||||
colorProgress = getColor(R.styleable.ProgressButton_progressColor, colorProgress)
|
?: context.getThemeColorStateList(materialR.attr.colorPrimaryContainer) ?: colorBase
|
||||||
colorText = getColor(R.styleable.ProgressButton_textColor, colorText)
|
colorProgress = getColorStateList(R.styleable.ProgressButton_progressColor)
|
||||||
textViewTitle.setTextColor(colorText)
|
?: context.getThemeColorStateList(materialR.attr.colorPrimary) ?: colorProgress
|
||||||
textViewSubtitle.setTextColor(colorText)
|
getColorStateList(R.styleable.ProgressButton_android_textColor)?.let { colorText ->
|
||||||
|
textViewTitle.setTextColor(colorText)
|
||||||
|
textViewSubtitle.setTextColor(colorText)
|
||||||
|
}
|
||||||
progress = getInt(R.styleable.ProgressButton_android_progress, 0).toFloat() /
|
progress = getInt(R.styleable.ProgressButton_android_progress, 0).toFloat() /
|
||||||
getInt(R.styleable.ProgressButton_android_max, 100).toFloat()
|
getInt(R.styleable.ProgressButton_android_max, 100).toFloat()
|
||||||
}
|
}
|
||||||
@@ -87,16 +97,25 @@ class ProgressButton @JvmOverloads constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
paint.style = Paint.Style.FILL
|
paint.style = Paint.Style.FILL
|
||||||
paint.color = colorProgress
|
|
||||||
paint.alpha = 84 // 255 * 0.33F
|
|
||||||
applyGravity()
|
applyGravity()
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
canvas.drawColor(colorBase)
|
canvas.drawColor(colorBaseCurrent)
|
||||||
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
|
if (progress > 0f) {
|
||||||
|
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) {
|
||||||
@@ -112,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) {
|
||||||
@@ -125,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
|
||||||
@@ -38,6 +39,18 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
|||||||
private var currentState = STATE_UP
|
private var currentState = STATE_UP
|
||||||
private var behavior = HideBottomNavigationOnScrollBehavior()
|
private var behavior = HideBottomNavigationOnScrollBehavior()
|
||||||
|
|
||||||
|
var isPinned: Boolean
|
||||||
|
get() = behavior.isPinned
|
||||||
|
set(value) {
|
||||||
|
behavior.isPinned = value
|
||||||
|
if (value) {
|
||||||
|
translationX = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isShownOrShowing: Boolean
|
||||||
|
get() = isVisible && currentState == STATE_UP
|
||||||
|
|
||||||
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
||||||
return behavior
|
return behavior
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.withStyledAttributes
|
import androidx.core.content.withStyledAttributes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
@@ -103,16 +104,22 @@ class TipView @JvmOverloads constructor(
|
|||||||
|
|
||||||
fun setPrimaryButtonText(@StringRes resId: Int) {
|
fun setPrimaryButtonText(@StringRes resId: Int) {
|
||||||
binding.buttonPrimary.setTextAndVisible(resId)
|
binding.buttonPrimary.setTextAndVisible(resId)
|
||||||
|
updateButtonsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSecondaryButtonText(@StringRes resId: Int) {
|
fun setSecondaryButtonText(@StringRes resId: Int) {
|
||||||
binding.buttonSecondary.setTextAndVisible(resId)
|
binding.buttonSecondary.setTextAndVisible(resId)
|
||||||
|
updateButtonsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setIcon(@DrawableRes resId: Int) {
|
fun setIcon(@DrawableRes resId: Int) {
|
||||||
icon = ContextCompat.getDrawable(context, resId)
|
icon = ContextCompat.getDrawable(context, resId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateButtonsLayout() {
|
||||||
|
binding.layoutButtons.isVisible = binding.buttonPrimary.isVisible || binding.buttonSecondary.isVisible
|
||||||
|
}
|
||||||
|
|
||||||
interface OnButtonClickListener {
|
interface OnButtonClickListener {
|
||||||
|
|
||||||
fun onPrimaryButtonClick(tipView: TipView)
|
fun onPrimaryButtonClick(tipView: TipView)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.report
|
||||||
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class AcraCoroutineErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||||
|
CoroutineExceptionHandler {
|
||||||
|
|
||||||
|
override fun handleException(context: CoroutineContext, exception: Throwable) {
|
||||||
|
exception.printStackTraceDebug()
|
||||||
|
exception.report()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
@Deprecated("", replaceWith = ReplaceWith("CompositeMutex2"))
|
|
||||||
class CompositeMutex<T : Any> : Set<T> {
|
|
||||||
|
|
||||||
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
override val size: Int
|
|
||||||
get() = state.size
|
|
||||||
|
|
||||||
override fun contains(element: T): Boolean {
|
|
||||||
return state.containsKey(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsAll(elements: Collection<T>): Boolean {
|
|
||||||
return elements.all { x -> state.containsKey(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEmpty(): Boolean {
|
|
||||||
return state.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterator(): Iterator<T> {
|
|
||||||
return state.keys.iterator()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun lock(element: T) {
|
|
||||||
while (coroutineContext.isActive) {
|
|
||||||
waitForRemoval(element)
|
|
||||||
mutex.withLock {
|
|
||||||
if (state[element] == null) {
|
|
||||||
state[element] = MutableStateFlow(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unlock(element: T) {
|
|
||||||
checkNotNull(state.remove(element)) {
|
|
||||||
"CompositeMutex is not locked for $element"
|
|
||||||
}.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun waitForRemoval(element: T) {
|
|
||||||
val flow = state[element] ?: return
|
|
||||||
flow.first { it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
class CompositeRunnable(
|
|
||||||
private val children: List<Runnable>,
|
|
||||||
) : Runnable, Collection<Runnable> by children {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
for (child in children) {
|
|
||||||
child.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ class Event<T>(
|
|||||||
|
|
||||||
suspend fun consume(collector: FlowCollector<T>) {
|
suspend fun consume(collector: FlowCollector<T>) {
|
||||||
if (!isConsumed) {
|
if (!isConsumed) {
|
||||||
collector.emit(data)
|
|
||||||
isConsumed = true
|
isConsumed = true
|
||||||
|
collector.emit(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
||||||
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class IncognitoModeIndicator @Inject constructor(
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) : DefaultActivityLifecycleCallbacks {
|
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
|
||||||
if (activity !is AppCompatActivity) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
settings.observeAsFlow(
|
|
||||||
key = AppSettings.KEY_INCOGNITO_MODE,
|
|
||||||
valueProducer = { isIncognitoModeEnabled },
|
|
||||||
).flowOn(Dispatchers.IO)
|
|
||||||
.flowWithLifecycle(activity.lifecycle)
|
|
||||||
.onEach { updateStatusBar(activity, it) }
|
|
||||||
.launchIn(activity.lifecycleScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateStatusBar(activity: AppCompatActivity, isIncognitoModeEnabled: Boolean) {
|
|
||||||
activity.window.statusBarColor = if (isIncognitoModeEnabled) {
|
|
||||||
ContextCompat.getColor(activity, R.color.status_bar_incognito)
|
|
||||||
} else {
|
|
||||||
activity.getThemeColor(android.R.attr.statusBarColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,20 +2,22 @@ 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 CompositeMutex2<T : Any> : Set<T> {
|
class MultiMutex<T : Any> : Set<T> {
|
||||||
|
|
||||||
private val delegates = ArrayMap<T, Mutex>()
|
private val delegates = ArrayMap<T, Mutex>()
|
||||||
|
|
||||||
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 CompositeMutex2<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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,11 +37,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,12 +62,12 @@ 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
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
val Context.activityManager: ActivityManager?
|
val Context.activityManager: ActivityManager?
|
||||||
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
|
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
|
||||||
@@ -138,10 +141,13 @@ 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(
|
||||||
context.getThemeColor(com.google.android.material.R.attr.colorSurfaceContainer, alphaFactor),
|
context.getThemeColor(materialR.attr.colorSurfaceContainer, alphaFactor),
|
||||||
elevation,
|
elevation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -262,6 +268,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
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
@@ -12,9 +14,11 @@ import coil.request.SuccessResult
|
|||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
|
||||||
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
||||||
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? {
|
fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? {
|
||||||
val current = CoilUtils.result(this)
|
val current = CoilUtils.result(this)
|
||||||
@@ -43,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()
|
||||||
@@ -85,6 +80,17 @@ fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder {
|
|||||||
return tag(MangaSource::class.java, source)
|
return tag(MangaSource::class.java, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Builder {
|
||||||
|
val errorColor = ColorUtils.blendARGB(
|
||||||
|
context.getThemeColor(materialR.attr.colorErrorContainer),
|
||||||
|
context.getThemeColor(materialR.attr.colorBackgroundFloating),
|
||||||
|
0.25f,
|
||||||
|
)
|
||||||
|
return placeholder(AnimatedPlaceholderDrawable(context))
|
||||||
|
.fallback(ColorDrawable(context.getThemeColor(materialR.attr.colorSurfaceContainer)))
|
||||||
|
.error(ColorDrawable(errorColor))
|
||||||
|
}
|
||||||
|
|
||||||
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
|
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
|
||||||
val existing = build().listener
|
val existing = build().listener
|
||||||
return listener(
|
return listener(
|
||||||
|
|||||||
@@ -68,3 +68,5 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
|
|||||||
toList()
|
toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import androidx.lifecycle.LifecycleDestroyedException
|
import androidx.lifecycle.LifecycleDestroyedException
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
@@ -10,17 +9,20 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
val processLifecycleScope: LifecycleCoroutineScope
|
val processLifecycleScope: CoroutineScope
|
||||||
inline get() = ProcessLifecycleOwner.get().lifecycleScope
|
get() = ProcessLifecycleOwner.get().lifecycleScope + AcraCoroutineErrorHandler()
|
||||||
|
|
||||||
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
|
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
|
||||||
inline get() = RetainedLifecycleCoroutineScope(this)
|
inline get() = RetainedLifecycleCoroutineScope(this)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,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.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
owner.repeatOnLifecycle(minState) {
|
||||||
collect {
|
collect {
|
||||||
it?.consume(collector)
|
it?.consume(collector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
||||||
val b = Bundle(size)
|
val b = Bundle(size)
|
||||||
@@ -33,26 +28,6 @@ fun Fragment.addMenuProvider(provider: MenuProvider) {
|
|||||||
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
|
||||||
suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner {
|
|
||||||
val liveData = viewLifecycleOwnerLiveData
|
|
||||||
liveData.value?.let { return it }
|
|
||||||
return suspendCancellableCoroutine { cont ->
|
|
||||||
val observer = object : Observer<LifecycleOwner?> {
|
|
||||||
override fun onChanged(value: LifecycleOwner?) {
|
|
||||||
if (value != null) {
|
|
||||||
liveData.removeObserver(this)
|
|
||||||
cont.resume(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
liveData.observeForever(observer)
|
|
||||||
cont.invokeOnCancellation {
|
|
||||||
liveData.removeObserver(observer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
||||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.util.CompositeRunnable
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||||
if (obj == null || !isInstance(obj)) {
|
if (obj == null || !isInstance(obj)) {
|
||||||
@@ -9,15 +7,3 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
|||||||
}
|
}
|
||||||
return obj as T
|
return obj as T
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CompositeRunnable */
|
|
||||||
|
|
||||||
operator fun Runnable.plus(other: Runnable): Runnable {
|
|
||||||
val list = ArrayList<Runnable>(this.size + other.size)
|
|
||||||
if (this is CompositeRunnable) list.addAll(this) else list.add(this)
|
|
||||||
if (other is CompositeRunnable) list.addAll(other) else list.add(other)
|
|
||||||
return CompositeRunnable(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Runnable.size: Int
|
|
||||||
get() = if (this is CompositeRunnable) size else 1
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.text.StaticLayout
|
|
||||||
import androidx.core.graphics.withTranslation
|
|
||||||
|
|
||||||
fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
|
||||||
canvas.withTranslation(x, y) {
|
|
||||||
draw(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ import org.jsoup.HttpStatusException
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
|
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
|
||||||
import org.koitharu.kotatsu.core.exceptions.CaughtException
|
import org.koitharu.kotatsu.core.exceptions.CaughtException
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
||||||
@@ -38,6 +39,7 @@ private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported"
|
|||||||
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
||||||
is AuthRequiredException -> resources.getString(R.string.auth_required)
|
is AuthRequiredException -> resources.getString(R.string.auth_required)
|
||||||
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
|
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
|
||||||
|
is CloudFlareBlockedException -> resources.getString(R.string.blocked_by_server_message)
|
||||||
is ActivityNotFoundException,
|
is ActivityNotFoundException,
|
||||||
is UnsupportedOperationException,
|
is UnsupportedOperationException,
|
||||||
-> resources.getString(R.string.operation_not_supported)
|
-> resources.getString(R.string.operation_not_supported)
|
||||||
@@ -79,6 +81,8 @@ fun Throwable.getDisplayIcon() = when (this) {
|
|||||||
is SocketTimeoutException,
|
is SocketTimeoutException,
|
||||||
-> R.drawable.ic_plug_large
|
-> R.drawable.ic_plug_large
|
||||||
|
|
||||||
|
is CloudFlareBlockedException -> R.drawable.ic_denied_large
|
||||||
|
|
||||||
else -> R.drawable.ic_error_large
|
else -> R.drawable.ic_error_large
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.createViewModelLazy
|
import androidx.fragment.app.createViewModelLazy
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelStore
|
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
@@ -19,7 +17,3 @@ inline fun <reified VM : ViewModel> Fragment.parentFragmentViewModels(
|
|||||||
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
||||||
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
||||||
)
|
)
|
||||||
|
|
||||||
val ViewModelStore.values: Collection<ViewModel>
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
get() = this.keys().mapNotNull { get(it) }
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ suspend fun WorkManager.getWorkSpec(id: UUID): WorkSpec? = suspendCoroutine { co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
suspend fun WorkManager.getWorkInputData(id: UUID): Data? = getWorkSpec(id)?.input
|
suspend fun WorkManager.getWorkInputData(id: UUID): Data? = getWorkSpec(id)?.input
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.util.ext.peek
|
import org.koitharu.kotatsu.core.util.ext.peek
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||||
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
||||||
@@ -25,7 +27,9 @@ 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.util.recoverNotNull
|
import org.koitharu.kotatsu.parsers.util.recoverNotNull
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.Tracker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
class DetailsLoadUseCase @Inject constructor(
|
class DetailsLoadUseCase @Inject constructor(
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
@@ -33,6 +37,7 @@ class DetailsLoadUseCase @Inject constructor(
|
|||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val recoverUseCase: RecoverMangaUseCase,
|
private val recoverUseCase: RecoverMangaUseCase,
|
||||||
private val imageGetter: Html.ImageGetter,
|
private val imageGetter: Html.ImageGetter,
|
||||||
|
private val trackerProvider: Provider<Tracker>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
||||||
@@ -49,6 +54,7 @@ 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(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) {
|
||||||
@@ -90,4 +96,10 @@ class DetailsLoadUseCase @Inject constructor(
|
|||||||
}
|
}
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun updateTracker(details: Manga) = runCatchingCancellable {
|
||||||
|
trackerProvider.get().syncWithDetails(details)
|
||||||
|
}.onFailure { e ->
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user