diff --git a/app/build.gradle b/app/build.gradle
index c7065c93a..dafd6b900 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,16 +7,16 @@ plugins {
}
android {
- compileSdkVersion 32
- buildToolsVersion '32.0.0'
+ compileSdkVersion 33
+ buildToolsVersion '33.0.0'
namespace 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
- targetSdkVersion 32
- versionCode 490
- versionName '4.0-a1'
+ targetSdkVersion 33
+ versionCode 493
+ versionName '4.0-a4'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -29,6 +29,8 @@ android {
// define this values in your local.properties file
buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\""
buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\""
+ resValue "string", "acra_login", "${localProperty('acra.login')}"
+ resValue "string", "acra_password", "${localProperty('acra.password')}"
}
buildTypes {
debug {
@@ -80,7 +82,7 @@ afterEvaluate {
}
}
dependencies {
- implementation('com.github.KotatsuApp:kotatsu-parsers:8709c3dd0c') {
+ implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') {
exclude group: 'org.json', module: 'json'
}
@@ -88,7 +90,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.1'
- implementation 'androidx.fragment:fragment-ktx:1.5.1'
+ implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
@@ -100,7 +102,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
- implementation 'com.google.android.material:material:1.7.0-alpha03'
+ implementation 'com.google.android.material:material:1.7.0-beta01'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
@@ -115,18 +117,19 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
- implementation "com.google.dagger:hilt-android:2.42"
- kapt "com.google.dagger:hilt-compiler:2.42"
+ implementation "com.google.dagger:hilt-android:2.43.2"
+ kapt "com.google.dagger:hilt-compiler:2.43.2"
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
- implementation 'io.coil-kt:coil-base:2.1.0'
- implementation 'io.coil-kt:coil-svg:2.1.0'
- implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
+ implementation 'io.coil-kt:coil-base:2.2.0'
+ implementation 'io.coil-kt:coil-svg:2.2.0'
+// implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
+ implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2942b797a2'
implementation 'com.github.solkin:disk-lru-cache:1.4'
- implementation 'ch.acra:acra-mail:5.9.5'
- implementation 'ch.acra:acra-dialog:5.9.5'
+ implementation 'ch.acra:acra-http:5.9.6'
+ implementation 'ch.acra:acra-dialog:5.9.6'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
@@ -144,6 +147,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.4.3'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
- androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42'
- kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42'
+ androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
+ kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2'
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index fb3509dc2..3be2f59ae 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -10,4 +10,7 @@
}
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
--dontwarn okhttp3.internal.platform.ConscryptPlatform
\ No newline at end of file
+-dontwarn okhttp3.internal.platform.ConscryptPlatform
+
+-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
+-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5917b03bc..e5d92b0e3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
+
(KEY_MANGA)?.manga,
+ manga = intent?.getParcelableExtraCompat(KEY_MANGA)?.manga,
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
- uri = intent?.data
+ uri = intent?.data,
)
constructor(args: Bundle?) : this(
- manga = args?.getParcelable(KEY_MANGA)?.manga,
+ manga = args?.getParcelableCompat(KEY_MANGA)?.manga,
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
- uri = null
+ uri = null,
)
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt
index e1e328c8f..8866cc068 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt
@@ -21,14 +21,14 @@ abstract class AlertDialogFragment : DialogFragment() {
viewBinding = binding
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(binding.root)
- .also(::onBuildDialog)
+ .run(::onBuildDialog)
.create()
}
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
- savedInstanceState: Bundle?
+ savedInstanceState: Bundle?,
) = viewBinding?.root
@CallSuper
@@ -37,9 +37,9 @@ abstract class AlertDialogFragment : DialogFragment() {
super.onDestroyView()
}
- open fun onBuildDialog(builder: MaterialAlertDialogBuilder) = Unit
+ open fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder = builder
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt
index 591fd6b99..36b5e0e5f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt
@@ -8,16 +8,18 @@ import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
+import kotlin.math.hypot
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.measureWidth
-import kotlin.math.hypot
class BubbleAnimator(
private val bubble: View,
) {
- private val animationDuration = (bubble.resources.getInteger(android.R.integer.config_shortAnimTime) *
- bubble.context.animatorDurationScale).toLong()
+ private val animationDuration = (
+ bubble.resources.getInteger(android.R.integer.config_shortAnimTime) *
+ bubble.context.animatorDurationScale
+ ).toLong()
private var animator: Animator? = null
private var isHiding = false
@@ -65,12 +67,12 @@ class BubbleAnimator(
private var isCancelled = false
- override fun onAnimationCancel(animation: Animator?) {
+ override fun onAnimationCancel(animation: Animator) {
super.onAnimationCancel(animation)
isCancelled = true
}
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@BubbleAnimator.animator) {
bubble.isInvisible = true
@@ -79,4 +81,4 @@ class BubbleAnimator(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt
index a00fc90b9..0a87f5262 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt
@@ -14,8 +14,10 @@ class ScrollbarAnimator(
private val scrollbarPaddingEnd: Float,
) {
- private val animationDuration = (scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) *
- scrollbar.context.animatorDurationScale).toLong()
+ private val animationDuration = (
+ scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) *
+ scrollbar.context.animatorDurationScale
+ ).toLong()
private var animator: ViewPropertyAnimator? = null
private var isHiding = false
@@ -40,30 +42,32 @@ class ScrollbarAnimator(
}
animator?.cancel()
isHiding = true
- animator = scrollbar
- .animate()
- .translationX(scrollbarPaddingEnd)
- .alpha(0f)
- .setDuration(animationDuration)
- .setListener(HideListener())
+ animator = scrollbar.animate().apply {
+ translationX(scrollbarPaddingEnd)
+ alpha(0f)
+ duration = animationDuration
+ setListener(HideListener(this))
+ }
}
- private inner class HideListener : AnimatorListenerAdapter() {
+ private inner class HideListener(
+ private val viewPropertyAnimator: ViewPropertyAnimator,
+ ) : AnimatorListenerAdapter() {
private var isCancelled = false
- override fun onAnimationCancel(animation: Animator?) {
+ override fun onAnimationCancel(animation: Animator) {
super.onAnimationCancel(animation)
isCancelled = true
}
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
- if (!isCancelled && animation === this@ScrollbarAnimator.animator) {
+ if (!isCancelled && this@ScrollbarAnimator.animator === viewPropertyAnimator) {
scrollbar.isInvisible = true
isHiding = false
this@ScrollbarAnimator.animator = null
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
index 3aaa7270d..cd60b79f8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
@@ -2,15 +2,11 @@ package org.koitharu.kotatsu.base.ui.widgets
import android.animation.LayoutTransition
import android.content.Context
-import android.transition.AutoTransition
-import android.transition.TransitionManager
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
-import android.view.animation.AccelerateDecelerateInterpolator
-import android.view.animation.DecelerateInterpolator
import androidx.annotation.AttrRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
@@ -30,6 +26,8 @@ import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import org.koitharu.kotatsu.utils.ext.parents
+private const val THROTTLE_DELAY = 200L
+
class BottomSheetHeaderBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -39,11 +37,17 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
private val bottomSheetCallback = Callback()
+ private val adjustStateRunnable = Runnable { adjustState() }
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
private val locationBuffer = IntArray(2)
private val expansionListeners = LinkedList()
private var fitStatusBar = false
- private var transition: AutoTransition? = null
+ private val minHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_min)
+ private val maxHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_max)
+ private var isLayoutSuppressedCompat = false
+ private var isLayoutCalledWhileSuppressed = false
+ private var isBsExpanded = false
+ private var stateAdjustedAt = 0L
@Deprecated("")
val toolbar: MaterialToolbar
@@ -156,6 +160,14 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
binding.toolbar.setSubtitle(resId)
}
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ if (isLayoutSuppressedCompat) {
+ isLayoutCalledWhileSuppressed = true
+ } else {
+ super.onLayout(changed, l, t, r, b)
+ }
+ }
+
private fun setBottomSheetBehavior(behavior: BottomSheetBehavior<*>?) {
bottomSheetBehavior?.removeBottomSheetCallback(bottomSheetCallback)
bottomSheetBehavior = behavior
@@ -166,15 +178,20 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
private fun onBottomSheetStateChanged(newState: Int) {
- val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
- if (isExpanded == binding.dragHandle.isGone) {
- return
+ val expanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
+ if (isBsExpanded != expanded) {
+ isBsExpanded = expanded
+ postAdjustState()
}
- TransitionManager.beginDelayedTransition(this, getTransition())
- binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null)
- binding.dragHandle.isGone = isExpanded
- expansionListeners.forEach { it.onExpansionStateChanged(this, isExpanded) }
- dispatchInsets(ViewCompat.getRootWindowInsets(this))
+ }
+
+ private fun suppressLayoutCompat(suppress: Boolean) {
+ if (suppress == isLayoutSuppressedCompat) return
+ isLayoutSuppressedCompat = suppress
+ if (!suppress && isLayoutCalledWhileSuppressed) {
+ requestLayout()
+ }
+ isLayoutCalledWhileSuppressed = false
}
private fun dispatchInsets(insets: WindowInsetsCompat?) {
@@ -182,11 +199,14 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
return
}
val isExpanded = binding.dragHandle.isGone
+ val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
if (isExpanded) {
- val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
updatePadding(top = topInset)
} else {
updatePadding(top = 0)
+ binding.dragHandle.updateLayoutParams {
+ height = topInset.coerceIn(minHandleHeight, maxHandleHeight)
+ }
}
}
@@ -225,7 +245,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
return true
}
val viewId = child.id
- return viewId == R.id.dragHandle || viewId == R.id.toolbar || viewId == R.id.frame
+ return viewId == R.id.dragHandle || viewId == R.id.toolbar
}
private fun convertLayoutParams(params: ViewGroup.LayoutParams?): Toolbar.LayoutParams? {
@@ -242,13 +262,24 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
}
- private fun getTransition(): AutoTransition {
- transition?.let { return it }
- val t = AutoTransition()
- t.duration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
- t.addTarget(binding.dragHandle)
- transition = t
- return t
+ private fun postAdjustState() {
+ removeCallbacks(adjustStateRunnable)
+ val now = System.currentTimeMillis()
+ if (stateAdjustedAt + THROTTLE_DELAY < now) {
+ adjustState()
+ } else {
+ postDelayed(adjustStateRunnable, THROTTLE_DELAY)
+ }
+ }
+
+ private fun adjustState() {
+ suppressLayoutCompat(true)
+ binding.toolbar.navigationIcon = (if (isBsExpanded) closeDrawable else null)
+ binding.dragHandle.isGone = isBsExpanded
+ expansionListeners.forEach { it.onExpansionStateChanged(this, isBsExpanded) }
+ dispatchInsets(ViewCompat.getRootWindowInsets(this))
+ stateAdjustedAt = System.currentTimeMillis()
+ suppressLayoutCompat(false)
}
private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener {
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt
index f012d9a95..3e9e7b55d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt
@@ -100,7 +100,7 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
.applySystemAnimatorScale(context)
.setListener(
object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
currentAnimator = null
postInvalidate()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt
index 4c6d48a55..87adf5a1c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt
@@ -42,7 +42,7 @@ class BookmarksViewModel @Inject constructor(
BookmarksGroup(manga, bookmarks)
}
}
- .catch { e -> e.toErrorState(canRetry = false) }
+ .catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
fun removeBookmarks(ids: Map>) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
index 683d8abb0..dc18898b3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
@@ -55,11 +55,12 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud
override fun onDestroyView() {
binding.webView.stopLoading()
+ binding.webView.destroy()
super.onDestroyView()
}
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder.setNegativeButton(android.R.string.cancel, null)
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
}
override fun onResume() {
@@ -83,7 +84,7 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud
override fun onCheckPassed() {
pendingResult.putBoolean(EXTRA_RESULT, true)
- dismiss()
+ dismissAllowingStateLoss()
}
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt
index ade35613b..ee0da6165 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
+import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
@@ -8,8 +9,8 @@ import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
interface TrackLogsDao {
@Transaction
- @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
- suspend fun findAll(offset: Int, limit: Int): List
+ @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
+ fun observeAll(limit: Int): Flow>
@Query("DELETE FROM track_logs")
suspend fun clear()
@@ -25,4 +26,4 @@ interface TrackLogsDao {
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt
index af938a813..5960cb5e5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.db.entity
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
@@ -10,7 +11,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
fun TagEntity.toMangaTag() = MangaTag(
key = this.key,
title = this.title.toTitleCase(),
- source = MangaSource.valueOf(this.source),
+ source = MangaSource(this.source) ?: MangaSource.DUMMY,
)
fun Collection.toMangaTags() = mapToSet(TagEntity::toMangaTag)
@@ -19,7 +20,7 @@ fun MangaEntity.toManga(tags: Set) = Manga(
id = this.id,
title = this.title,
altTitle = this.altTitle,
- state = this.state?.let { MangaState.valueOf(it) },
+ state = this.state?.let { MangaState(it) },
rating = this.rating,
isNsfw = this.isNsfw,
url = this.url,
@@ -27,8 +28,8 @@ fun MangaEntity.toManga(tags: Set) = Manga(
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
- source = MangaSource.valueOf(this.source),
- tags = tags
+ source = MangaSource(this.source) ?: MangaSource.DUMMY,
+ tags = tags,
)
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
@@ -54,14 +55,18 @@ fun MangaTag.toEntity() = TagEntity(
title = title,
key = key,
source = source.name,
- id = "${key}_${source.name}".longHashCode()
+ id = "${key}_${source.name}".longHashCode(),
)
fun Collection.toEntities() = map(MangaTag::toEntity)
// Other
-@Suppress("FunctionName")
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
SortOrder.valueOf(name)
-}.getOrDefault(fallback)
\ No newline at end of file
+}.getOrDefault(fallback)
+
+@Suppress("FunctionName")
+fun MangaState(name: String): MangaState? = runCatching {
+ MangaState.valueOf(name)
+}.getOrNull()
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt
index 62ea43d64..84bc96d21 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt
@@ -11,8 +11,10 @@ import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.BuildConfig
@@ -53,11 +55,11 @@ class AppUpdateRepository @Inject constructor(
}
}
- suspend fun fetchUpdate(): AppVersion? {
+ suspend fun fetchUpdate(): AppVersion? = withContext(Dispatchers.Default) {
if (!isUpdateSupported()) {
- return null
+ return@withContext null
}
- return runCatching {
+ runCatching {
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
val available = getAvailableVersions().asArrayList()
available.sortBy { it.versionId }
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt
index c7eb5bbf9..eefe100a7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt
@@ -2,10 +2,12 @@ package org.koitharu.kotatsu.core.os
import android.app.ActivityManager
import android.content.Context
+import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.media.ThumbnailUtils
import android.os.Build
import android.util.Size
+import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@@ -22,6 +24,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
+import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
@@ -35,13 +38,18 @@ class ShortcutsUpdater @Inject constructor(
private val coil: ImageLoader,
private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository,
-) : InvalidationTracker.Observer(TABLE_HISTORY) {
+ private val settings: AppSettings,
+) : InvalidationTracker.Observer(TABLE_HISTORY), SharedPreferences.OnSharedPreferenceChangeListener {
private val iconSize by lazy { getIconSize(context) }
private var shortcutsUpdateJob: Job? = null
- override fun onInvalidated(tables: MutableSet) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
+ init {
+ settings.subscribe(this)
+ }
+
+ override fun onInvalidated(tables: Set) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || !settings.isDynamicShortcutsEnabled) {
return
}
val prevJob = shortcutsUpdateJob
@@ -51,6 +59,16 @@ class ShortcutsUpdater @Inject constructor(
}
}
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && key == AppSettings.KEY_SHORTCUTS) {
+ if (settings.isDynamicShortcutsEnabled) {
+ onInvalidated(emptySet())
+ } else {
+ clearShortcuts()
+ }
+ }
+ }
+
suspend fun requestPinShortcut(manga: Manga): Boolean {
return ShortcutManagerCompat.requestPinShortcut(
context,
@@ -64,6 +82,15 @@ class ShortcutsUpdater @Inject constructor(
return shortcutsUpdateJob?.join() != null
}
+ fun isDynamicShortcutsAvailable(): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
+ return false
+ }
+ val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
+ return manager.maxShortcutCountPerActivity > 0
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatching {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
@@ -74,6 +101,15 @@ class ShortcutsUpdater @Inject constructor(
it.printStackTraceDebug()
}
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
+ private fun clearShortcuts() {
+ val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
+ try {
+ manager.removeAllDynamicShortcuts()
+ } catch (_: IllegalStateException) {
+ }
+ }
+
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching {
val bmp = coil.execute(
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index 53360ca0a..7eeb64804 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
@@ -11,6 +11,12 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.qualifiers.ApplicationContext
+import java.io.File
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+import javax.inject.Inject
+import javax.inject.Singleton
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
@@ -19,12 +25,6 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue
import org.koitharu.kotatsu.utils.ext.toUriOrNull
-import java.io.File
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-import java.util.*
-import javax.inject.Inject
-import javax.inject.Singleton
@Singleton
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
@@ -65,6 +65,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val readerPageSwitch: Set
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
+ val isReaderTapsAdaptive: Boolean
+ get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
+
var isTrafficWarningEnabled: Boolean
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
@@ -73,13 +76,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
- val isUpdateCheckingEnabled: Boolean
- get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true)
-
- var lastUpdateCheckTimestamp: Long
- get() = prefs.getLong(KEY_APP_UPDATE, 0L)
- set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) }
-
val isTrackerEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
@@ -141,6 +137,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isExitConfirmationEnabled: Boolean
get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false)
+ val isDynamicShortcutsEnabled: Boolean
+ get() = prefs.getBoolean(KEY_SHORTCUTS, true)
+
var sourcesOrder: List
get() = prefs.getString(KEY_SOURCES_ORDER, null)
?.split('|')
@@ -324,10 +323,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_INCOGNITO_MODE = "incognito"
const val KEY_SYNC = "sync"
const val KEY_READER_BAR = "reader_bar"
+ const val KEY_SHORTCUTS = "dynamic_shortcuts"
+ const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
// About
const val KEY_APP_UPDATE = "app_update"
- const val KEY_APP_UPDATE_AUTO = "app_update_auto"
const val KEY_APP_TRANSLATION = "about_app_translation"
private const val NETWORK_NEVER = 0
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/MangaErrorDialog.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/MangaErrorDialog.kt
new file mode 100644
index 000000000..b5781451a
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/MangaErrorDialog.kt
@@ -0,0 +1,72 @@
+package org.koitharu.kotatsu.core.ui
+
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.text.HtmlCompat
+import androidx.core.text.htmlEncode
+import androidx.core.text.parseAsHtml
+import androidx.fragment.app.FragmentManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.AlertDialogFragment
+import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
+import org.koitharu.kotatsu.databinding.DialogMangaErrorBinding
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.utils.ext.report
+import org.koitharu.kotatsu.utils.ext.requireParcelable
+import org.koitharu.kotatsu.utils.ext.requireSerializable
+import org.koitharu.kotatsu.utils.ext.withArgs
+
+class MangaErrorDialog : AlertDialogFragment() {
+
+ private lateinit var error: Throwable
+ private lateinit var manga: Manga
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val args = requireArguments()
+ manga = args.requireParcelable(ARG_MANGA).manga
+ error = args.requireSerializable(ARG_ERROR)
+ }
+
+ override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogMangaErrorBinding {
+ return DialogMangaErrorBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ with(binding.textViewMessage) {
+ movementMethod = LinkMovementMethod.getInstance()
+ text = context.getString(
+ R.string.manga_error_description_pattern,
+ this@MangaErrorDialog.error.message?.htmlEncode().orEmpty(),
+ manga.publicUrl,
+ ).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY)
+ }
+ }
+
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
+ .setCancelable(true)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.report) { _, _ ->
+ dismiss()
+ error.report(TAG)
+ }.setTitle(R.string.error_occurred)
+ }
+
+ companion object {
+
+ private const val TAG = "MangaErrorDialog"
+ private const val ARG_ERROR = "error"
+ private const val ARG_MANGA = "manga"
+
+ fun show(fm: FragmentManager, manga: Manga, error: Throwable) = MangaErrorDialog().withArgs(2) {
+ putParcelable(ARG_MANGA, ParcelableManga(manga, false))
+ putSerializable(ARG_ERROR, error)
+ }.show(fm, TAG)
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt
index d9ca082ee..1733523b7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt
@@ -35,13 +35,13 @@ class ChaptersMenuProvider(
else -> false
}
- override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
bottomSheetMediator?.lock()
return true
}
- override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
- (item?.actionView as? SearchView)?.setQuery("", false)
+ override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+ (item.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null)
bottomSheetMediator?.unlock()
return true
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index 25b43c7c2..9add56679 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -5,8 +5,13 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
+import android.transition.Slide
+import android.transition.TransitionManager
+import android.view.Gravity
import android.view.Menu
import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
@@ -16,6 +21,7 @@ import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -27,6 +33,7 @@ import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
+import org.koitharu.kotatsu.core.ui.MangaErrorDialog
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.download.ui.service.DownloadService
@@ -93,6 +100,7 @@ class DetailsActivity :
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError)
viewModel.onShowToast.observe(this) {
+ makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
}
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranchName.observe(this) {
@@ -158,8 +166,11 @@ class DetailsActivity :
private fun onMangaUpdated(manga: Manga) {
title = manga.title
- binding.buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
+ val hasChapters = !manga.chapters.isNullOrEmpty()
+ binding.buttonRead.isEnabled = hasChapters
invalidateOptionsMenu()
+ showBottomSheet(manga.chapters != null)
+ binding.groupHeader?.isVisible = hasChapters
}
private fun onMangaRemoved(manga: Manga) {
@@ -172,17 +183,17 @@ class DetailsActivity :
}
private fun onError(e: Throwable) {
+ val manga = viewModel.manga.value
when {
ExceptionResolver.canResolve(e) -> {
resolveError(e)
}
- viewModel.manga.value == null -> {
+ manga == null -> {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
}
else -> {
- val snackbar = Snackbar.make(
- binding.containerDetails,
+ val snackbar = makeSnackbar(
e.getDisplayMessage(resources),
if (viewModel.manga.value?.chapters == null) {
Snackbar.LENGTH_INDEFINITE
@@ -190,10 +201,9 @@ class DetailsActivity :
Snackbar.LENGTH_LONG
},
)
- snackbar.anchorView = binding.headerChapters
if (e.isReportable()) {
- snackbar.setAction(R.string.report) {
- e.report("DetailsActivity::onError")
+ snackbar.setAction(R.string.details) {
+ MangaErrorDialog.show(supportFragmentManager, manga, e)
}
}
snackbar.show()
@@ -238,8 +248,7 @@ class DetailsActivity :
fun showChapterMissingDialog(chapterId: Long) {
val remoteManga = viewModel.getRemoteManga()
if (remoteManga == null) {
- val snackbar = Snackbar.make(binding.containerDetails, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
- snackbar.anchorView = binding.headerChapters
+ val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
snackbar.show()
return
}
@@ -291,6 +300,24 @@ class DetailsActivity :
private fun isTabletLayout() = binding.layoutBottom == null
+ private fun showBottomSheet(isVisible: Boolean) {
+ val view = binding.layoutBottom ?: return
+ if (view.isVisible == isVisible) return
+ val transition = Slide(Gravity.BOTTOM)
+ transition.addTarget(view)
+ transition.interpolator = AccelerateDecelerateInterpolator()
+ TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transition)
+ view.isVisible = isVisible
+ }
+
+ private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
+ val sb = Snackbar.make(binding.containerDetails, text, duration)
+ if (binding.layoutBottom?.isVisible == true) {
+ sb.anchorView = binding.headerChapters
+ }
+ return sb
+ }
+
companion object {
fun newIntent(context: Context, manga: Manga): Intent {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
index 201a6a761..c3de6fa13 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
@@ -70,6 +70,7 @@ class DetailsFragment :
super.onViewCreated(view, savedInstanceState)
binding.textViewAuthor.setOnClickListener(this)
binding.imageViewCover.setOnClickListener(this)
+ binding.infoLayout.textViewSource.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
@@ -228,6 +229,14 @@ class DetailsFragment :
),
)
}
+ R.id.textView_source -> {
+ startActivity(
+ MangaListActivity.newIntent(
+ context = v.context,
+ source = manga.source,
+ ),
+ )
+ }
R.id.imageView_cover -> {
startActivity(
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }),
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt
index e15669f94..9e57bd7f0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt
@@ -28,4 +28,4 @@ fun MangaChapter.toListItem(
uploadDateMs = uploadDate,
dateFormat = dateFormat,
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt
index f6553c37a..0cdbc597e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.getSerializableCompat
@AndroidEntryPoint
class FavouritesCategoryEditActivity :
@@ -70,8 +71,8 @@ class FavouritesCategoryEditActivity :
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
- val order = savedInstanceState.getSerializable(KEY_SORT_ORDER)
- if (order != null && order is SortOrder) {
+ val order = savedInstanceState.getSerializableCompat(KEY_SORT_ORDER)
+ if (order != null) {
selectedSortOrder = order
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt
index cca1dffe6..85e7a45e6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
+import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class FavouritesListViewModel @AssistedInject constructor(
@@ -53,7 +54,7 @@ class FavouritesListViewModel @AssistedInject constructor(
} else {
repository.observeAll(categoryId)
},
- createListModeFlow()
+ createListModeFlow(),
) { list, mode ->
when {
list.isEmpty() -> listOf(
@@ -66,13 +67,13 @@ class FavouritesListViewModel @AssistedInject constructor(
R.string.favourites_category_empty
},
actionStringRes = 0,
- )
+ ),
)
else -> list.toUi(mode, this)
}
}.catch {
emit(listOf(it.toErrorState(canRetry = false)))
- }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
+ }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
if (categoryId != NO_ID) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
index cbf38db4b..c9f851f62 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
@@ -23,7 +23,7 @@ import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
-import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst
@@ -60,8 +60,8 @@ class HistoryListViewModel @Inject constructor(
}.onFirst {
loadingCounter.decrement()
}.catch {
- it.toErrorState(canRetry = false)
- }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
+ emit(listOf(it.toErrorState(canRetry = false)))
+ }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
override fun onRefresh() = Unit
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt
index fdf161282..243e8cb5d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt
@@ -56,17 +56,17 @@ class ReadingProgressView @JvmOverloads constructor(
getProgressDrawable().progress = p
}
- override fun onAnimationStart(animation: Animator?) = Unit
+ override fun onAnimationStart(animation: Animator) = Unit
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
if (percentAnimator === animation) {
percentAnimator = null
}
}
- override fun onAnimationCancel(animation: Animator?) = Unit
+ override fun onAnimationCancel(animation: Animator) = Unit
- override fun onAnimationRepeat(animation: Animator?) = Unit
+ override fun onAnimationRepeat(animation: Animator) = Unit
fun setPercent(value: Float, animate: Boolean) {
val currentDrawable = peekProgressDrawable()
diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt
index 46e318b1f..140be0638 100644
--- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt
@@ -27,7 +27,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
-import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.daysDiff
private const val HISTORY_MAX_SEGMENTS = 2
@@ -49,8 +49,8 @@ class LibraryViewModel @Inject constructor(
) { history, favourites ->
mapList(history, favourites)
}.catch { e ->
- e.toErrorState(canRetry = false)
- }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
+ emit(listOf(e.toErrorState(canRetry = false)))
+ }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
override suspend fun getCounter(mangaId: Long): Int {
return trackingRepository.getNewChaptersCount(mangaId)
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt
index 443f7021d..64747aedd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt
@@ -6,6 +6,7 @@ import android.view.View
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.view.updateLayoutParams
+import androidx.core.widget.TextViewCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlin.math.roundToInt
@@ -46,12 +47,12 @@ class ItemSizeResolver(resources: Resources, private val settings: AppSettings)
}
}
- override fun onViewAttachedToWindow(v: View?) {
+ override fun onViewAttachedToWindow(v: View) {
settings.subscribe(this)
update()
}
- override fun onViewDetachedFromWindow(v: View?) {
+ override fun onViewDetachedFromWindow(v: View) {
settings.unsubscribe(this)
}
@@ -77,7 +78,7 @@ class ItemSizeResolver(resources: Resources, private val settings: AppSettings)
}
if (textAppearanceResId != prevTextAppearance) {
prevTextAppearance = textAppearanceResId
- setTextAppearance(textAppearanceResId)
+ TextViewCompat.setTextAppearance(this, textAppearanceResId)
requestLayout()
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
index 1c988732a..3728c57b2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
@@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
@@ -17,7 +18,6 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.DialogListModeBinding
import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
-import javax.inject.Inject
@AndroidEntryPoint
class ListModeSelectDialog :
@@ -33,8 +33,9 @@ class ListModeSelectDialog :
container: ViewGroup?,
) = DialogListModeBinding.inflate(inflater, container, false)
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder.setTitle(R.string.list_mode)
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
+ .setTitle(R.string.list_mode)
.setPositiveButton(R.string.done, null)
.setCancelable(true)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
index 19b3f11f7..6b1fe569f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.list.ui.filter
+import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
class FilterAdapter(
listener: OnFilterChangedListener,
+ listListener: ListListener,
) : AsyncListDifferDelegationAdapter(
FilterDiffCallback(),
filterSortDelegate(listener),
@@ -11,4 +13,9 @@ class FilterAdapter(
filterHeaderDelegate(),
filterLoadingDelegate(),
filterErrorDelegate(),
-)
\ No newline at end of file
+) {
+
+ init {
+ differ.addListListener(listListener)
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt
index c80f7358a..e7be8c8b6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt
@@ -6,18 +6,21 @@ import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.FragmentManager
-import dagger.hilt.android.AndroidEntryPoint
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.LinearLayoutManager
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
+import org.koitharu.kotatsu.utils.ext.isScrolledToTop
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
class FilterBottomSheet :
BaseBottomSheet(),
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
- DialogInterface.OnKeyListener {
+ DialogInterface.OnKeyListener,
+ AsyncListDiffer.ListListener {
private val viewModel by parentFragmentViewModels()
@@ -33,13 +36,13 @@ class FilterBottomSheet :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val adapter = FilterAdapter(viewModel)
+ val adapter = FilterAdapter(viewModel, this)
binding.recyclerView.adapter = adapter
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
initOptionsMenu()
}
- override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
return true
}
@@ -71,6 +74,12 @@ class FilterBottomSheet :
return false
}
+ override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) {
+ if (currentList.size > previousList.size && view != null) {
+ (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
+ }
+ }
+
private fun initOptionsMenu() {
binding.headerBar.toolbar.inflateMenu(R.menu.opt_filter)
val searchMenuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search)
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
index 65e0b5658..7d08cb72b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
@@ -63,6 +63,10 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
x.tags.containsAll(tags)
}
}
+ when (sortOrder) {
+ SortOrder.ALPHABETICAL -> list.sortBy { it.title }
+ SortOrder.RATING -> list.sortBy { it.rating }
+ }
return list
}
@@ -250,7 +254,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
}
}
- override val sortOrders = setOf(SortOrder.ALPHABETICAL)
+ override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
override suspend fun getPageUrl(page: MangaPage) = page.url
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt
index 000e57c14..5cd6cf85b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt
@@ -27,8 +27,9 @@ class ImportDialogFragment : AlertDialogFragment(), View.On
return DialogImportBinding.inflate(inflater, container, false)
}
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder.setTitle(R.string._import)
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
+ .setTitle(R.string._import)
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(true)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
index 48349e846..507dafe25 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.main.ui
+import android.Manifest
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.os.Build
import android.os.Bundle
import android.util.SparseIntArray
import android.view.MenuItem
@@ -7,6 +10,7 @@ import android.view.View
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode
+import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
@@ -24,7 +28,6 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
@@ -291,12 +294,11 @@ class MainActivity :
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
}
+ requestNotificationsPermission()
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
- yield()
- // TODO get().requestFullSyncAndGc(get())
}
}
@@ -347,6 +349,15 @@ class MainActivity :
showNav(!isOpened)
}
+ private fun requestNotificationsPermission() {
+ if (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
+ }
+ }
+
private inner class VoiceInputCallback : ActivityResultCallback {
override fun onActivityResult(result: String?) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
index c60ae4aa8..0805d872e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
@@ -4,26 +4,28 @@ import android.util.SparseIntArray
import androidx.core.util.set
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
-import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
-import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
- private val settings: AppSettings,
private val appUpdateRepository: AppUpdateRepository,
private val trackingRepository: TrackingRepository,
+ syncController: SyncController,
+ database: MangaDatabase,
) : BaseViewModel() {
val onOpenReader = SingleLiveEvent()
@@ -43,9 +45,12 @@ class MainViewModel @Inject constructor(
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0))
init {
- launchJob(Dispatchers.Default) {
+ launchJob {
appUpdateRepository.fetchUpdate()
}
+ launchJob {
+ syncController.requestFullSyncAndGc(database)
+ }
}
fun openLastReader() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt
index a3b4bcbbd..c94cdd153 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt
@@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import javax.inject.Inject
import javax.inject.Singleton
+import org.acra.dialog.CrashReportDialog
import org.koitharu.kotatsu.core.prefs.AppSettings
@Singleton
@@ -14,7 +15,7 @@ class AppProtectHelper @Inject constructor(private val settings: AppSettings) :
private var isUnlocked = settings.appPassword.isNullOrEmpty()
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
- if (activity !is ProtectActivity && !isUnlocked) {
+ if (!isUnlocked && activity !is ProtectActivity && activity !is CrashReportDialog) {
val sourceIntent = Intent(activity, activity.javaClass)
activity.intent?.let {
sourceIntent.putExtras(it)
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt
index b3d0322bf..4a6886068 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityProtectBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
@AndroidEntryPoint
class ProtectActivity :
@@ -44,7 +45,7 @@ class ProtectActivity :
viewModel.onError.observe(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onUnlockSuccess.observe(this) {
- val intent = intent.getParcelableExtra(EXTRA_INTENT)
+ val intent = intent.getParcelableExtraCompat(EXTRA_INTENT)
startActivity(intent)
finishAfterTransition()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt
index 7d4eb83e1..9efa3cef6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt
@@ -6,6 +6,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+import kotlin.math.roundToInt
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -17,9 +19,8 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
+import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.withArgs
-import javax.inject.Inject
-import kotlin.math.roundToInt
@AndroidEntryPoint
class ChaptersBottomSheet : BaseBottomSheet(), OnListItemClickListener {
@@ -33,7 +34,7 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val chapters = arguments?.getParcelable(ARG_CHAPTERS)?.chapters
+ val chapters = arguments?.getParcelableCompat(ARG_CHAPTERS)?.chapters
if (chapters.isNullOrEmpty()) {
dismissAllowingStateLoss()
return
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
index 9986fde88..cb9a0f13c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
@@ -40,6 +40,7 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.GridTouchHelper
+import org.koitharu.kotatsu.utils.IdlingDetector
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.*
@@ -51,15 +52,18 @@ class ReaderActivity :
OnPageSelectListener,
ReaderConfigBottomSheet.Callback,
ReaderControlDelegate.OnInteractionListener,
- OnApplyWindowInsetsListener {
+ OnApplyWindowInsetsListener,
+ IdlingDetector.Callback {
@Inject
lateinit var viewModelFactory: ReaderViewModel.Factory
+ private val idlingDetector = IdlingDetector(TimeUnit.SECONDS.toMillis(10), this)
+
val viewModel by assistedViewModels {
viewModelFactory.create(
intent = MangaIntent(intent),
- initialState = intent?.getParcelableExtra(EXTRA_STATE),
+ initialState = intent?.getParcelableExtraCompat(EXTRA_STATE),
preselectedBranch = intent?.getStringExtra(EXTRA_BRANCH),
)
}
@@ -70,6 +74,9 @@ class ReaderActivity :
pageSwitchTimer.delaySec = value
}
+ override val readerMode: ReaderMode?
+ get() = readerManager.currentMode
+
private lateinit var pageSwitchTimer: PageSwitchTimer
private lateinit var touchHelper: GridTouchHelper
private lateinit var controlDelegate: ReaderControlDelegate
@@ -84,11 +91,12 @@ class ReaderActivity :
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
pageSwitchTimer = PageSwitchTimer(this, this)
- controlDelegate = ReaderControlDelegate(lifecycleScope, settings, this)
+ controlDelegate = ReaderControlDelegate(settings, this, this)
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
binding.slider.setLabelFormatter(PageLabelFormatter())
ReaderSliderListener(this, viewModel).attachToSlider(binding.slider)
insetsDelegate.interceptingWindowInsetsListener = this
+ idlingDetector.bindToLifecycle(this)
viewModel.onError.observe(this, this::onError)
viewModel.readerMode.observe(this, this::onInitReader)
@@ -111,6 +119,11 @@ class ReaderActivity :
override fun onUserInteraction() {
super.onUserInteraction()
pageSwitchTimer.onUserInteraction()
+ idlingDetector.onUserInteraction()
+ }
+
+ override fun onIdle() {
+ viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
}
private fun onInitReader(mode: ReaderMode) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt
index 916b0ee5f..890c9074b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt
@@ -15,14 +15,15 @@ import org.koitharu.kotatsu.databinding.DialogReaderConfigBinding
import org.koitharu.kotatsu.utils.ext.withArgs
@Deprecated("Not in use")
-class ReaderConfigDialog : AlertDialogFragment(),
+class ReaderConfigDialog :
+ AlertDialogFragment(),
CheckableButtonGroup.OnCheckedChangeListener {
private lateinit var mode: ReaderMode
override fun onInflateView(
inflater: LayoutInflater,
- container: ViewGroup?
+ container: ViewGroup?,
) = DialogReaderConfigBinding.inflate(inflater, container, false)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -32,8 +33,9 @@ class ReaderConfigDialog : AlertDialogFragment(),
?: ReaderMode.STANDARD
}
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder.setTitle(R.string.read_mode)
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
+ .setTitle(R.string.read_mode)
.setPositiveButton(R.string.done, null)
.setCancelable(true)
}
@@ -48,8 +50,10 @@ class ReaderConfigDialog : AlertDialogFragment(),
}
override fun onDismiss(dialog: DialogInterface) {
- ((parentFragment as? Callback)
- ?: (activity as? Callback))?.onReaderModeChanged(mode)
+ (
+ (parentFragment as? Callback)
+ ?: (activity as? Callback)
+ )?.onReaderModeChanged(mode)
super.onDismiss(dialog)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt
index 98c14c98c..3da6ed8d1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt
@@ -1,33 +1,39 @@
package org.koitharu.kotatsu.reader.ui
+import android.content.SharedPreferences
import android.view.KeyEvent
import android.view.SoundEffectConstants
import android.view.View
-import androidx.lifecycle.LifecycleCoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.utils.GridTouchHelper
class ReaderControlDelegate(
- scope: LifecycleCoroutineScope,
- settings: AppSettings,
+ private val settings: AppSettings,
private val listener: OnInteractionListener,
-) {
+ owner: LifecycleOwner,
+) : DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener {
private var isTapSwitchEnabled: Boolean = true
private var isVolumeKeysSwitchEnabled: Boolean = false
+ private var isReaderTapsAdaptive: Boolean = true
init {
- settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch }
- .flowOn(Dispatchers.Default)
- .onEach {
- isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it
- isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it
- }.launchIn(scope)
+ owner.lifecycle.addObserver(this)
+ settings.subscribe(this)
+ updateSettings()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ settings.unsubscribe(this)
+ owner.lifecycle.removeObserver(this)
+ super.onDestroy(owner)
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ updateSettings()
}
fun onGridTouch(area: Int, view: View) {
@@ -41,7 +47,7 @@ class ReaderControlDelegate(
view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP)
}
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
- listener.switchPageBy(-1)
+ listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT)
}
GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) {
@@ -49,7 +55,7 @@ class ReaderControlDelegate(
view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN)
}
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
- listener.switchPageBy(1)
+ listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT)
}
}
@@ -72,19 +78,25 @@ class ReaderControlDelegate(
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN,
- KeyEvent.KEYCODE_DPAD_RIGHT,
-> {
listener.switchPageBy(1)
true
}
+ KeyEvent.KEYCODE_DPAD_RIGHT -> {
+ listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
+ true
+ }
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP,
- KeyEvent.KEYCODE_DPAD_LEFT,
-> {
listener.switchPageBy(-1)
true
}
+ KeyEvent.KEYCODE_DPAD_LEFT -> {
+ listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
+ true
+ }
KeyEvent.KEYCODE_DPAD_CENTER -> {
listener.toggleUiVisibility()
true
@@ -99,8 +111,21 @@ class ReaderControlDelegate(
)
}
+ private fun updateSettings() {
+ val switch = settings.readerPageSwitch
+ isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in switch
+ isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in switch
+ isReaderTapsAdaptive = settings.isReaderTapsAdaptive
+ }
+
+ private fun isReaderTapsReversed(): Boolean {
+ return isReaderTapsAdaptive && listener.readerMode == ReaderMode.REVERSED
+ }
+
interface OnInteractionListener {
+ val readerMode: ReaderMode?
+
fun switchPageBy(delta: Int)
fun toggleUiVisibility()
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt
index 5f825fee6..cd1e31a8d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt
@@ -8,17 +8,19 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
-import android.icu.text.SimpleDateFormat
import android.util.AttributeSet
import android.view.View
import androidx.annotation.AttrRes
import androidx.core.graphics.ColorUtils
import com.google.android.material.R as materialR
+import java.text.SimpleDateFormat
import java.util.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.ext.getThemeColor
+import org.koitharu.kotatsu.utils.ext.measureDimension
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.resolveDp
class ReaderInfoBarView @JvmOverloads constructor(
@@ -29,23 +31,46 @@ class ReaderInfoBarView @JvmOverloads constructor(
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textBounds = Rect()
- private val inset = context.resources.resolveDp(2f)
private val timeFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT)
private val timeReceiver = TimeReceiver()
+ private var insetLeft: Int = 0
+ private var insetRight: Int = 0
+ private var insetTop: Int = 0
+ private val colorText = ColorUtils.setAlphaComponent(
+ context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK),
+ 200,
+ )
+ private val colorOutline = ColorUtils.setAlphaComponent(
+ context.getThemeColor(materialR.attr.colorSurface, Color.WHITE),
+ 200,
+ )
private var timeText = timeFormat.format(Date())
private var text: String = ""
private val innerHeight
- get() = height - inset - inset - paddingTop - paddingBottom
+ get() = height - paddingTop - paddingBottom - insetTop
private val innerWidth
- get() = width - inset - inset - paddingLeft - paddingRight
+ get() = width - paddingLeft - paddingRight - insetLeft - insetRight
init {
- paint.color = ColorUtils.setAlphaComponent(
- context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK),
- 160,
+ paint.strokeWidth = context.resources.resolveDp(2f)
+ val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding")
+ val insetStart = getSystemUiDimensionOffset("status_bar_padding_start") + insetCorner
+ val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end") + insetCorner
+ val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+ insetLeft = if (isRtl) insetEnd else insetStart
+ insetRight = if (isRtl) insetStart else insetEnd
+ insetTop = minOf(insetLeft, insetRight)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight + insetLeft + insetRight
+ val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom + insetTop
+ setMeasuredDimension(
+ measureDimension(desiredWidth, widthMeasureSpec),
+ measureDimension(desiredHeight, heightMeasureSpec),
)
}
@@ -53,9 +78,9 @@ class ReaderInfoBarView @JvmOverloads constructor(
super.onDraw(canvas)
val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom
paint.textAlign = Paint.Align.LEFT
- canvas.drawText(text, paddingLeft + inset, paddingTop + inset + ty, paint)
+ canvas.drawTextOutline(text, (paddingLeft + insetLeft).toFloat(), paddingTop + insetTop + ty)
paint.textAlign = Paint.Align.RIGHT
- canvas.drawText(timeText, width - paddingRight - inset, paddingTop + inset + ty, paint)
+ canvas.drawTextOutline(timeText, (width - paddingRight - insetRight).toFloat(), paddingTop + insetTop + ty)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@@ -103,6 +128,15 @@ class ReaderInfoBarView @JvmOverloads constructor(
paint.getTextBounds(str, 0, str.length, textBounds)
}
+ private fun Canvas.drawTextOutline(text: String, x: Float, y: Float) {
+ paint.color = colorOutline
+ paint.style = Paint.Style.STROKE
+ drawText(text, x, y, paint)
+ paint.color = colorText
+ paint.style = Paint.Style.FILL
+ drawText(text, x, y, paint)
+ }
+
private inner class TimeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@@ -110,4 +144,13 @@ class ReaderInfoBarView @JvmOverloads constructor(
invalidate()
}
}
+
+ private fun getSystemUiDimensionOffset(name: String): Int = runCatching {
+ val manager = context.packageManager
+ val resources = manager.getResourcesForApplication("com.android.systemui")
+ val resId = resources.getIdentifier(name, "dimen", "com.android.systemui")
+ resources.getDimensionPixelOffset(resId)
+ }.onFailure {
+ it.printStackTraceDebug()
+ }.getOrDefault(0)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt
index 52894b4da..6bd234afd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt
@@ -8,6 +8,7 @@ import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
+import org.koitharu.kotatsu.utils.ext.getParcelableCompat
private const val KEY_STATE = "state"
@@ -18,7 +19,7 @@ abstract class BaseReader : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- var restoredState = savedInstanceState?.getParcelable(KEY_STATE)
+ var restoredState = savedInstanceState?.getParcelableCompat(KEY_STATE)
viewModel.content.observe(viewLifecycleOwner) {
onPagesChanged(it.pages, restoredState ?: it.state)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
index d097c1bc2..0529c8ebe 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
@@ -4,12 +4,12 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.ext.resetTransformations
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
@Suppress("LeakingThis")
abstract class BaseReaderAdapter>(
@@ -45,7 +45,7 @@ abstract class BaseReaderAdapter>(
final override fun onCreateViewHolder(
parent: ViewGroup,
- viewType: Int
+ viewType: Int,
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
suspend fun setItems(items: List) = suspendCoroutine { cont ->
@@ -58,7 +58,7 @@ abstract class BaseReaderAdapter>(
parent: ViewGroup,
loader: PageLoader,
settings: AppSettings,
- exceptionResolver: ExceptionResolver
+ exceptionResolver: ExceptionResolver,
): H
private class DiffCallback : DiffUtil.ItemCallback() {
@@ -70,6 +70,5 @@ abstract class BaseReaderAdapter>(
override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
return oldItem == newItem
}
-
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
index a4d60c861..397ce0b4e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
@@ -69,7 +69,7 @@ open class PageHolder(
override fun onImageShowing(zoom: ZoomMode) {
binding.ssiv.maxScale = 2f * maxOf(
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
- binding.ssiv.height / binding.ssiv.sHeight.toFloat()
+ binding.ssiv.height / binding.ssiv.sHeight.toFloat(),
)
when (zoom) {
ZoomMode.FIT_CENTER -> {
@@ -81,7 +81,7 @@ open class PageHolder(
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
binding.ssiv.setScaleAndCenter(
binding.ssiv.minScale,
- PointF(0f, binding.ssiv.sHeight / 2f)
+ PointF(0f, binding.ssiv.sHeight / 2f),
)
}
ZoomMode.FIT_WIDTH -> {
@@ -89,14 +89,14 @@ open class PageHolder(
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
binding.ssiv.setScaleAndCenter(
binding.ssiv.minScale,
- PointF(binding.ssiv.sWidth / 2f, 0f)
+ PointF(binding.ssiv.sWidth / 2f, 0f),
)
}
ZoomMode.KEEP_START -> {
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
binding.ssiv.setScaleAndCenter(
binding.ssiv.maxScale,
- PointF(0f, 0f)
+ PointF(0f, 0f),
)
}
}
@@ -115,9 +115,9 @@ open class PageHolder(
override fun onError(e: Throwable) {
bindingInfo.textViewError.text = e.getDisplayMessage(context.resources)
bindingInfo.buttonRetry.setText(
- ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again }
+ ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
)
bindingInfo.layoutError.isVisible = true
bindingInfo.progressBar.hideCompat()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt
index 96740a971..fe9122989 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
+import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -52,7 +53,7 @@ class PagesThumbnailsSheet :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val pages = arguments?.getParcelable(ARG_PAGES)?.pages
+ val pages = arguments?.getParcelableCompat(ARG_PAGES)?.pages
if (pages.isNullOrEmpty()) {
dismissAllowingStateLoss()
return
diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
index 9148a373b..2779c5d33 100644
--- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
@@ -99,7 +99,7 @@ class RemoteListFragment : MangaListFragment() {
override fun onQueryTextChange(newText: String?): Boolean = false
- override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
return true
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt
index f203f2e4c..d0be7d23c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt
@@ -10,10 +10,13 @@ private const val USER_AGENT_SHIKIMORI = "Kotatsu"
class ShikimoriInterceptor(private val storage: ShikimoriStorage) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
- val request = chain.request().newBuilder()
+ val sourceRequest = chain.request()
+ val request = sourceRequest.newBuilder()
request.header(CommonHeaders.USER_AGENT, USER_AGENT_SHIKIMORI)
- storage.accessToken?.let {
- request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
+ if (!sourceRequest.url.pathSegments.contains("oauth")) {
+ storage.accessToken?.let {
+ request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
+ }
}
val response = chain.proceed(request.build())
if (!response.isSuccessful && !response.isRedirect) {
@@ -21,4 +24,4 @@ class ShikimoriInterceptor(private val storage: ShikimoriStorage) : Interceptor
}
return response
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt
index 2fcd12718..2267994ec 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt
@@ -40,13 +40,14 @@ class ShikimoriRepository(
suspend fun authorize(code: String?) {
val body = FormBody.Builder()
- body.add("grant_type", "authorization_code")
body.add("client_id", BuildConfig.SHIKIMORI_CLIENT_ID)
body.add("client_secret", BuildConfig.SHIKIMORI_CLIENT_SECRET)
if (code != null) {
+ body.add("grant_type", "authorization_code")
body.add("redirect_uri", REDIRECT_URI)
body.add("code", code)
} else {
+ body.add("grant_type", "refresh_token")
body.add("refresh_token", checkNotNull(storage.refreshToken))
}
val request = Request.Builder()
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt
index 06045eae9..151452acb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikiMangaSelectionDe
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikimoriSelectorAdapter
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.requireParcelable
import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
@@ -47,7 +48,7 @@ class ScrobblingSelectorBottomSheet :
private val viewModel by assistedViewModels {
viewModelFactory.create(
- requireNotNull(requireArguments().getParcelable(MangaIntent.KEY_MANGA)).manga,
+ requireArguments().requireParcelable(MangaIntent.KEY_MANGA).manga,
)
}
@@ -84,7 +85,7 @@ class ScrobblingSelectorBottomSheet :
dismiss()
}
viewModel.searchQuery.observe(viewLifecycleOwner) {
- binding.headerBar.toolbar.subtitle = it
+ binding.headerBar.subtitle = it
}
}
@@ -102,7 +103,7 @@ class ScrobblingSelectorBottomSheet :
viewModel.loadList(append = true)
}
- override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
return true
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
index b3eca260d..6247a3707 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
+import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
@AndroidEntryPoint
class MangaListActivity :
@@ -29,7 +30,7 @@ class MangaListActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
- val tags = intent.getParcelableExtra(EXTRA_TAGS)?.tags
+ val tags = intent.getParcelableExtraCompat(EXTRA_TAGS)?.tags
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val source = intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: tags?.firstOrNull()?.source
if (source == null) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
deleted file mode 100644
index 7d8cccdd8..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-package org.koitharu.kotatsu.settings
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import androidx.activity.ComponentActivity
-import androidx.annotation.MainThread
-import androidx.core.net.toUri
-import com.google.android.material.R as materialR
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import java.io.ByteArrayInputStream
-import java.io.InputStream
-import java.security.MessageDigest
-import java.security.cert.CertificateFactory
-import java.security.cert.X509Certificate
-import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.koitharu.kotatsu.BuildConfig
-import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.core.github.AppUpdateRepository
-import org.koitharu.kotatsu.core.github.AppVersion
-import org.koitharu.kotatsu.core.github.VersionId
-import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
-import org.koitharu.kotatsu.utils.FileSize
-import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
-
-@Deprecated("")
-class AppUpdateChecker(private val activity: ComponentActivity) {
-
- private val settings: AppSettings = TODO()
- private val repo: AppUpdateRepository = TODO()
-
- suspend fun checkIfNeeded(): Boolean? = if (
- settings.isUpdateCheckingEnabled &&
- settings.lastUpdateCheckTimestamp + PERIOD < System.currentTimeMillis()
- ) {
- checkNow()
- } else {
- null
- }
-
- suspend fun checkNow() = runCatching {
- val version = repo.fetchUpdate() ?: return@runCatching false
- val newVersionId = VersionId(version.name)
- val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
- val result = newVersionId > currentVersionId
- if (result) {
- withContext(Dispatchers.Main) {
- showUpdateDialog(version)
- }
- }
- settings.lastUpdateCheckTimestamp = System.currentTimeMillis()
- result
- }.onFailure {
- it.printStackTraceDebug()
- }.getOrNull()
-
- @MainThread
- private fun showUpdateDialog(version: AppVersion) {
- val message = buildString {
- append(activity.getString(R.string.new_version_s, version.name))
- appendLine()
- append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize)))
- appendLine()
- appendLine()
- append(version.description)
- }
- MaterialAlertDialogBuilder(activity, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
- .setTitle(R.string.app_update_available)
- .setMessage(message)
- .setIcon(R.drawable.ic_app_update)
- .setPositiveButton(R.string.download) { _, _ ->
- val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri())
- activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser)))
- }
- .setNegativeButton(R.string.close, null)
- .setCancelable(false)
- .create()
- .show()
- }
-
- companion object {
-
- private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
- private val PERIOD = TimeUnit.HOURS.toMillis(6)
-
- fun isUpdateSupported(context: Context): Boolean {
- return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1
- }
-
- @Suppress("DEPRECATION")
- @SuppressLint("PackageManagerGetSignatures")
- private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching {
- val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
- val signatures = requireNotNull(packageInfo?.signatures)
- val cert: ByteArray = signatures.first().toByteArray()
- val input: InputStream = ByteArrayInputStream(cert)
- val cf = CertificateFactory.getInstance("X509")
- val c = cf.generateCertificate(input) as X509Certificate
- val md: MessageDigest = MessageDigest.getInstance("SHA1")
- val publicKey: ByteArray = md.digest(c.encoded)
- return publicKey.byte2HexFormatted()
- }.onFailure { error ->
- error.printStackTraceDebug()
- }.getOrNull()
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
index 038e07edf..4389c39a6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.AndroidCookieJar
+import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -41,8 +42,13 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
@Inject
lateinit var cookieJar: AndroidCookieJar
+ @Inject
+ lateinit var shortcutsUpdater: ShortcutsUpdater
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history)
+ findPreference(AppSettings.KEY_SHORTCUTS)?.isVisible =
+ shortcutsUpdater.isDynamicShortcutsAvailable()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
index 3436ca259..eefc9ba6d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -53,7 +55,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
super.onViewCreated(view, savedInstanceState)
findPreference(KEY_AUTH)?.run {
if (isVisible) {
- loadUsername(this)
+ loadUsername(viewLifecycleOwner, this)
}
}
}
@@ -68,7 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
}
}
- private fun loadUsername(preference: Preference) = viewLifecycleScope.launch {
+ private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
runCatching {
preference.summary = null
withContext(Dispatchers.Default) {
@@ -99,7 +101,8 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
val pref = findPreference(KEY_AUTH) ?: return@launch
- loadUsername(pref)
+ val lifecycleOwner = awaitViewLifecycle()
+ loadUsername(lifecycleOwner, pref)
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt
index 985ca0d1a..199cc513e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt
@@ -2,34 +2,41 @@ package org.koitharu.kotatsu.settings.about
import android.content.Intent
import android.os.Bundle
+import android.view.View
import androidx.core.net.toUri
+import androidx.fragment.app.viewModels
import androidx.preference.Preference
-import kotlinx.coroutines.launch
+import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
+import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.settings.AppUpdateChecker
-import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
+ private val viewModel by viewModels()
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_about)
- val isUpdateSupported = AppUpdateChecker.isUpdateSupported(requireContext())
- findPreference(AppSettings.KEY_APP_UPDATE_AUTO)?.run {
- isVisible = isUpdateSupported
- }
findPreference(AppSettings.KEY_APP_VERSION)?.run {
title = getString(R.string.app_version, BuildConfig.VERSION_NAME)
- isEnabled = isUpdateSupported
+ isEnabled = viewModel.isUpdateSupported
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.isLoading.observe(viewLifecycleOwner) {
+ findPreference(AppSettings.KEY_APP_UPDATE)?.isEnabled = !it
}
+ viewModel.onUpdateAvailable.observe(viewLifecycleOwner, ::onUpdateAvailable)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_APP_VERSION -> {
- checkForUpdates()
+ viewModel.checkForUpdates()
true
}
AppSettings.KEY_APP_TRANSLATION -> {
@@ -40,24 +47,12 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
}
}
- private fun checkForUpdates() {
- viewLifecycleScope.launch {
- findPreference(AppSettings.KEY_APP_VERSION)?.run {
- setSummary(R.string.checking_for_updates)
- isSelectable = false
- }
- val result = AppUpdateChecker(activity ?: return@launch).checkNow()
- findPreference(AppSettings.KEY_APP_VERSION)?.run {
- setSummary(
- when (result) {
- true -> R.string.check_for_updates
- false -> R.string.no_update_available
- null -> R.string.update_check_failed
- }
- )
- isSelectable = true
- }
+ private fun onUpdateAvailable(version: AppVersion?) {
+ if (version == null) {
+ Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show()
+ return
}
+ AppUpdateDialog(context ?: return).show(version)
}
private fun openLink(url: String, title: CharSequence?) {
@@ -68,7 +63,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
Intent.createChooser(intent, title)
} else {
intent
- }
+ },
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt
new file mode 100644
index 000000000..5289f751c
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt
@@ -0,0 +1,24 @@
+package org.koitharu.kotatsu.settings.about
+
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.core.github.AppUpdateRepository
+import org.koitharu.kotatsu.core.github.AppVersion
+import org.koitharu.kotatsu.utils.SingleLiveEvent
+
+@HiltViewModel
+class AboutSettingsViewModel @Inject constructor(
+ private val appUpdateRepository: AppUpdateRepository,
+) : BaseViewModel() {
+
+ val isUpdateSupported = appUpdateRepository.isUpdateSupported()
+ val onUpdateAvailable = SingleLiveEvent()
+
+ fun checkForUpdates() {
+ launchLoadingJob {
+ val update = appUpdateRepository.fetchUpdate()
+ onUpdateAvailable.call(update)
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt
new file mode 100644
index 000000000..4752a7461
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt
@@ -0,0 +1,39 @@
+package org.koitharu.kotatsu.settings.about
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+import com.google.android.material.R as materialR
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.github.AppVersion
+import org.koitharu.kotatsu.utils.FileSize
+
+class AppUpdateDialog(private val context: Context) {
+
+ fun show(version: AppVersion) {
+ val message = buildString {
+ append(context.getString(R.string.new_version_s, version.name))
+ appendLine()
+ append(context.getString(R.string.size_s, FileSize.BYTES.format(context, version.apkSize)))
+ appendLine()
+ appendLine()
+ append(version.description)
+ }
+ MaterialAlertDialogBuilder(
+ context,
+ materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
+ )
+ .setTitle(R.string.app_update_available)
+ .setMessage(message)
+ .setIcon(R.drawable.ic_app_update)
+ .setPositiveButton(R.string.download) { _, _ ->
+ val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri())
+ context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_in_browser)))
+ }
+ .setNegativeButton(R.string.close, null)
+ .setCancelable(false)
+ .create()
+ .show()
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt
index 65d0a0d47..825575ad9 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt
@@ -51,8 +51,9 @@ class BackupDialogFragment : AlertDialogFragment() {
viewModel.onError.observe(viewLifecycleOwner, this::onError)
}
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder.setCancelable(false)
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
+ .setCancelable(false)
.setNegativeButton(android.R.string.cancel, null)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt
index 5af836d4b..68edfe874 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt
@@ -44,8 +44,9 @@ class RestoreDialogFragment : AlertDialogFragment() {
viewModel.onError.observe(viewLifecycleOwner, this::onError)
}
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder.setCancelable(false)
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
+ .setCancelable(false)
}
private fun onError(e: Throwable) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt
index c500a8527..ac855f1e1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt
@@ -43,8 +43,8 @@ class NewSourcesDialogFragment :
viewModel.sources.observe(viewLifecycleOwner) { adapter.items = it }
}
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ return super.onBuildDialog(builder)
.setPositiveButton(R.string.done, this)
.setCancelable(true)
.setTitle(R.string.remote_sources)
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt
index 6cb8eb25e..74620b78f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt
@@ -39,8 +39,8 @@ class OnboardDialogFragment :
container: ViewGroup?,
) = DialogOnboardBinding.inflate(inflater, container, false)
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
- builder
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
+ super.onBuildDialog(builder)
.setPositiveButton(R.string.done, this)
.setCancelable(true)
if (isWelcome) {
@@ -50,6 +50,7 @@ class OnboardDialogFragment :
.setTitle(R.string.remote_sources)
.setNegativeButton(android.R.string.cancel, this)
}
+ return builder
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt
index 816d9d31b..e1962a000 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt
@@ -120,7 +120,7 @@ class SourcesSettingsFragment :
else -> false
}
- override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
return true
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt
index 58b9d6658..8cb75ead8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.databinding.FragmentToolsBinding
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.settings.SettingsActivity
+import org.koitharu.kotatsu.settings.about.AppUpdateDialog
import org.koitharu.kotatsu.settings.tools.model.StorageUsage
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.getThemeColor
@@ -68,6 +69,10 @@ class ToolsFragment :
intent.data = url.toUri()
startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser)))
}
+ R.id.card_update -> {
+ val version = viewModel.appUpdate.value ?: return
+ AppUpdateDialog(v.context).show(version)
+ }
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt
index d8d19d243..6b98d88db 100644
--- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
-import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.onFirst
@HiltViewModel
@@ -44,8 +44,8 @@ class SuggestionsViewModel @Inject constructor(
}.onFirst {
loadingCounter.decrement()
}.catch {
- it.toErrorState(canRetry = false)
- }.asLiveDataDistinct(
+ emit(listOf(it.toErrorState(canRetry = false)))
+ }.asFlowLiveData(
viewModelScope.coroutineContext + Dispatchers.Default,
listOf(LoadingState),
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt
index 452f60f8c..9f8099612 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt
@@ -5,9 +5,26 @@ import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
-fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
- id = trackLog.id,
- chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
- manga = manga.toManga(tags.toMangaTags()),
- createdAt = Date(trackLog.createdAt)
-)
\ No newline at end of file
+fun TrackLogWithManga.toTrackingLogItem(counters: MutableMap): TrackingLogItem {
+ val chaptersList = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() }
+ return TrackingLogItem(
+ id = trackLog.id,
+ chapters = chaptersList,
+ manga = manga.toManga(tags.toMangaTags()),
+ createdAt = Date(trackLog.createdAt),
+ isNew = counters.decrement(trackLog.mangaId, chaptersList.size),
+ )
+}
+
+private fun MutableMap.decrement(key: Long, count: Int): Boolean {
+ val counter = get(key)
+ if (counter == null || counter <= 0) {
+ return false
+ }
+ if (counter < count) {
+ remove(key)
+ } else {
+ put(key, counter - count)
+ }
+ return true
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt
index 118480f75..0eb482202 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt
@@ -18,6 +18,10 @@ abstract class TracksDao {
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun findNewChapters(mangaId: Long): Int?
+ @MapInfo(keyColumn = "manga_id", valueColumn = "chapters_new")
+ @Query("SELECT manga_id, chapters_new FROM tracks")
+ abstract fun observeNewChaptersMap(): Flow