diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml
index 69983d414..c640ff3c0 100644
--- a/.github/ISSUE_TEMPLATE/report_issue.yml
+++ b/.github/ISSUE_TEMPLATE/report_issue.yml
@@ -44,7 +44,7 @@ body:
label: Kotatsu version
description: You can find your Kotatsu version in **Settings → About**.
placeholder: |
- Example: "3.2"
+ Example: "3.2.3"
validations:
required: true
@@ -87,7 +87,7 @@ body:
required: true
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
required: true
- - label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**.
+ - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml
index a46a0648f..bae1b501c 100644
--- a/.github/ISSUE_TEMPLATE/request_feature.yml
+++ b/.github/ISSUE_TEMPLATE/request_feature.yml
@@ -33,7 +33,7 @@ body:
required: true
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
required: true
- - label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**.
+ - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d4fcf16ce..3ba4daee9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/kotlinScripting.xml
+/.idea/deploymentTargetDropDown.xml
.DS_Store
/build
/captures
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 27370aa28..000000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index a6fb1fbe4..2bcd23609 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -4,6 +4,9 @@
+
+
+
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9cec1af6c..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-language: android
-dist: trusty
-android:
- components:
- - android-30
- - build-tools-30.0.3
- - platform-tools-30.0.5
- - tools
-before_install:
- - yes | sdkmanager "platforms;android-30"
-script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
\ No newline at end of file
diff --git a/README.md b/README.md
index b079e87af..cea436ae3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Kotatsu is a free and open source manga reader for Android.
-  [](https://travis-ci.org/nv95/Kotatsu)  [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669) [](https://discord.gg/NNJ5RgVBC5)
+   [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669) [](https://discord.gg/NNJ5RgVBC5)
### Download
diff --git a/app/build.gradle b/app/build.gradle
index ff599eb34..e7442c75e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 32
- versionCode 405
- versionName '3.2.1'
+ versionCode 407
+ versionName '3.2.3'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -49,14 +49,15 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
- '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
- '-opt-in=kotlinx.coroutines.FlowPreview',
- '-opt-in=kotlin.contracts.ExperimentalContracts',
+ '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
+ '-opt-in=kotlinx.coroutines.FlowPreview',
+ '-opt-in=kotlin.contracts.ExperimentalContracts',
+ '-opt-in=coil.annotation.ExperimentalCoilApi',
]
}
lint {
abortOnError false
- disable 'MissingTranslation', 'PrivateResource'
+ disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
}
testOptions {
unitTests.includeAndroidResources = true
@@ -65,7 +66,7 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
- implementation('com.github.nv95:kotatsu-parsers:090ad4b256') {
+ implementation('com.github.nv95:kotatsu-parsers:05a93e2380') {
exclude group: 'org.json', module: 'json'
}
@@ -86,7 +87,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
- implementation 'com.google.android.material:material:1.6.0-rc01'
+ implementation 'com.google.android.material:material:1.7.0-alpha01'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
@@ -95,21 +96,22 @@ dependencies {
kapt 'androidx.room:room-compiler:2.4.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
- implementation 'com.squareup.okio:okio:3.0.0'
+ implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
+ implementation 'com.squareup.okio:okio:3.1.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
- implementation 'io.insert-koin:koin-android:3.1.6'
- implementation 'io.coil-kt:coil-base:1.4.0'
+ implementation 'io.insert-koin:koin-android:3.2.0'
+ implementation 'io.coil-kt:coil-base:2.0.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.4'
- debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
+ debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
- testImplementation 'io.insert-koin:koin-test-junit4:3.1.5'
+ testImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
diff --git a/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt
new file mode 100644
index 000000000..e00bb6a83
--- /dev/null
+++ b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt
@@ -0,0 +1,3 @@
+package org.koitharu.kotatsu.utils.ext
+
+fun Throwable.printStackTraceDebug() = printStackTrace()
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7e15a761e..2d3c76ebf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -83,7 +83,8 @@
+ android:label="@string/manga_shelf"
+ android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
@@ -101,8 +102,12 @@
+ android:launchMode="singleTop"
+ android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
+
diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
index 7ec6a7d0b..576e688c0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
@@ -7,6 +7,7 @@ import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
+import org.koitharu.kotatsu.bookmarks.bookmarksModule
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule
@@ -69,6 +70,7 @@ class KotatsuApp : Application() {
appWidgetModule,
suggestionsModule,
syncModule,
+ bookmarksModule,
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt
index 09a970eee..b3b32dc1f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt
@@ -9,54 +9,58 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
-import org.koitharu.kotatsu.parsers.util.medianOrNull
+import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
+import kotlin.math.roundToInt
object MangaUtils : KoinComponent {
+ private const val MIN_WEBTOON_RATIO = 2
+
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
- suspend fun determineMangaIsWebtoon(pages: List): Boolean? {
- try {
- val page = pages.medianOrNull() ?: return null
- val url = MangaRepository(page.source).getPageUrl(page)
- val uri = Uri.parse(url)
- val size = if (uri.scheme == "cbz") {
+ suspend fun determineMangaIsWebtoon(pages: List): Boolean {
+ val pageIndex = (pages.size * 0.3).roundToInt()
+ val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
+ val url = MangaRepository(page.source).getPageUrl(page)
+ val uri = Uri.parse(url)
+ val size = if (uri.scheme == "cbz") {
+ runInterruptible(Dispatchers.IO) {
+ val zip = ZipFile(uri.schemeSpecificPart)
+ val entry = zip.getEntry(uri.fragment)
+ zip.getInputStream(entry).use {
+ getBitmapSize(it)
+ }
+ }
+ } else {
+ val request = Request.Builder()
+ .url(url)
+ .get()
+ .header(CommonHeaders.REFERER, page.referer)
+ .cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
+ .build()
+ get().newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
- val zip = ZipFile(uri.schemeSpecificPart)
- val entry = zip.getEntry(uri.fragment)
- zip.getInputStream(entry).use {
- getBitmapSize(it)
- }
- }
- } else {
- val request = Request.Builder()
- .url(url)
- .get()
- .header(CommonHeaders.REFERER, page.referer)
- .cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
- .build()
- get().newCall(request).await().use {
- runInterruptible(Dispatchers.IO) {
- getBitmapSize(it.body?.byteStream())
- }
+ getBitmapSize(it.body?.byteStream())
}
}
- return size.width * 2 < size.height
- } catch (e: Exception) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
- return null
}
+ return size.width * MIN_WEBTOON_RATIO < size.height
+ }
+
+ suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
+ val options = BitmapFactory.Options().apply {
+ inJustDecodeBounds = true
+ }
+ BitmapFactory.decodeFile(file.path, options)?.recycle()
+ options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size {
@@ -69,4 +73,4 @@ object MangaUtils : KoinComponent {
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt
new file mode 100644
index 000000000..43c9bf7e4
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt
@@ -0,0 +1,19 @@
+package org.koitharu.kotatsu.base.domain
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.koitharu.kotatsu.utils.ext.processLifecycleScope
+
+fun interface ReversibleHandle {
+
+ suspend fun reverse()
+}
+
+fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
+ reverse()
+}
+
+operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
+ this.reverse()
+ other.reverse()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt
index 0672b880f..75503afc5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt
@@ -9,11 +9,12 @@ import android.view.ViewGroup.LayoutParams
import androidx.appcompat.app.AppCompatDialog
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
-import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
+import com.google.android.material.R as materialR
abstract class BaseBottomSheet : BottomSheetDialogFragment() {
@@ -43,7 +44,9 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (resources.getBoolean(R.bool.is_tablet)) {
AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog)
- } else super.onCreateDialog(savedInstanceState)
+ } else {
+ AppBottomSheetDialog(requireContext(), theme)
+ }
}
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt
index 64317e4a7..e43ca8877 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt
@@ -7,10 +7,12 @@ import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
+@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
@@ -18,7 +20,8 @@ private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
-abstract class BaseFullscreenActivity : BaseActivity(),
+abstract class BaseFullscreenActivity :
+ BaseActivity(),
View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity : BaseActivity(),
showSystemUI()
}
+ @Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java")
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
// TODO WindowInsetsControllerCompat works incorrect
+ @Suppress("DEPRECATION")
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
}
+ @Suppress("DEPRECATION")
protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
index 39233e28f..f17e3aa9f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
@@ -1,18 +1,25 @@
package org.koitharu.kotatsu.base.ui
+import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.*
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
import org.koitharu.kotatsu.utils.SingleLiveEvent
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class BaseViewModel : ViewModel() {
- val onError = SingleLiveEvent()
- val isLoading = CountedBooleanLiveData()
+ protected val loadingCounter = CountedBooleanLiveData()
+ protected val errorEvent = SingleLiveEvent()
+
+ val onError: LiveData
+ get() = errorEvent
+
+ val isLoading: LiveData
+ get() = loadingCounter
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
@@ -25,20 +32,18 @@ abstract class BaseViewModel : ViewModel() {
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
- isLoading.postValue(true)
+ loadingCounter.increment()
try {
block()
} finally {
- isLoading.postValue(false)
+ loadingCounter.decrement()
}
}
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
- if (BuildConfig.DEBUG) {
- throwable.printStackTrace()
- }
+ throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
- onError.postCall(throwable)
+ errorEvent.postCall(throwable)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt
new file mode 100644
index 000000000..d3b911ace
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt
@@ -0,0 +1,29 @@
+package org.koitharu.kotatsu.base.ui.dialog
+
+import android.content.Context
+import android.graphics.Color
+import android.view.View
+import com.google.android.material.bottomsheet.BottomSheetDialog
+
+class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
+
+ /**
+ * https://github.com/material-components/material-components-android/issues/2582
+ */
+ @Suppress("DEPRECATION")
+ override fun onAttachedToWindow() {
+ val window = window
+ val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
+ super.onAttachedToWindow()
+ if (window != null) {
+ // If the navigation bar is translucent at all, the BottomSheet should be edge to edge
+ val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
+ if (drawEdgeToEdge) {
+ // Copied from super.onAttachedToWindow:
+ val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ // Fix super-class's window flag bug by respecting the intial system UI visibility:
+ window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt
new file mode 100644
index 000000000..650e816c5
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt
@@ -0,0 +1,20 @@
+package org.koitharu.kotatsu.base.ui.list
+
+import android.view.View
+import android.view.View.OnClickListener
+import android.view.View.OnLongClickListener
+import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
+
+class AdapterDelegateClickListenerAdapter(
+ private val adapterDelegate: AdapterDelegateViewBindingViewHolder,
+ private val clickListener: OnListItemClickListener,
+) : OnClickListener, OnLongClickListener {
+
+ override fun onClick(v: View) {
+ clickListener.onItemClick(adapterDelegate.item, v)
+ }
+
+ override fun onLongClick(v: View): Boolean {
+ return clickListener.onItemLongClick(adapterDelegate.item, v)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt
index cb54ef7db..d654e541d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt
@@ -1,20 +1,31 @@
package org.koitharu.kotatsu.base.ui.util
-import androidx.lifecycle.MutableLiveData
+import androidx.annotation.AnyThread
+import androidx.lifecycle.LiveData
+import java.util.concurrent.atomic.AtomicInteger
-class CountedBooleanLiveData : MutableLiveData(false) {
+class CountedBooleanLiveData : LiveData(false) {
- private var counter = 0
+ private val counter = AtomicInteger(0)
- override fun setValue(value: Boolean) {
- if (value) {
- counter++
- } else {
- counter--
+ @AnyThread
+ fun increment() {
+ if (counter.getAndIncrement() == 0) {
+ postValue(true)
}
- val newValue = counter > 0
- if (newValue != this.value) {
- super.setValue(newValue)
+ }
+
+ @AnyThread
+ fun decrement() {
+ if (counter.decrementAndGet() == 0) {
+ postValue(false)
+ }
+ }
+
+ @AnyThread
+ fun reset() {
+ if (counter.getAndSet(0) != 0) {
+ postValue(false)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt
index 18d7262dc..bdaf8f476 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt
@@ -36,8 +36,7 @@ class ListItemTextView @JvmOverloads constructor(
init {
context.withStyledAttributes(attrs, R.styleable.ListItemTextView, defStyleAttr) {
- val itemRippleColor = getColorStateList(R.styleable.ListItemTextView_rippleColor)
- ?: getRippleColorFallback(context)
+ val itemRippleColor = getRippleColor(context)
val shape = createShapeDrawable(this)
background = RippleDrawable(
RippleUtils.sanitizeRippleDrawableColor(itemRippleColor),
@@ -108,7 +107,7 @@ class ListItemTextView @JvmOverloads constructor(
ta.getResourceId(R.styleable.ListItemTextView_shapeAppearanceOverlay, 0),
).build()
val shapeDrawable = MaterialShapeDrawable(shapeAppearance)
- shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundTint)
+ shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundFillColor)
return InsetDrawable(
shapeDrawable,
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetLeft, 0),
@@ -118,7 +117,7 @@ class ListItemTextView @JvmOverloads constructor(
)
}
- private fun getRippleColorFallback(context: Context): ColorStateList {
+ private fun getRippleColor(context: Context): ColorStateList {
return context.getThemeColorStateList(android.R.attr.colorControlHighlight)
?: ColorStateList.valueOf(Color.TRANSPARENT)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt
new file mode 100644
index 000000000..4a8294765
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt
@@ -0,0 +1,10 @@
+package org.koitharu.kotatsu.bookmarks
+
+import org.koin.dsl.module
+import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
+
+val bookmarksModule
+ get() = module {
+
+ factory { BookmarksRepository(get()) }
+ }
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt
new file mode 100644
index 000000000..0959b3362
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt
@@ -0,0 +1,28 @@
+package org.koitharu.kotatsu.bookmarks.data
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import org.koitharu.kotatsu.core.db.entity.MangaEntity
+
+@Entity(
+ tableName = "bookmarks",
+ primaryKeys = ["manga_id", "page_id"],
+ foreignKeys = [
+ ForeignKey(
+ entity = MangaEntity::class,
+ parentColumns = ["manga_id"],
+ childColumns = ["manga_id"],
+ onDelete = ForeignKey.CASCADE
+ ),
+ ]
+)
+class BookmarkEntity(
+ @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
+ @ColumnInfo(name = "page_id", index = true) val pageId: Long,
+ @ColumnInfo(name = "chapter_id") val chapterId: Long,
+ @ColumnInfo(name = "page") val page: Int,
+ @ColumnInfo(name = "scroll") val scroll: Int,
+ @ColumnInfo(name = "image") val imageUrl: String,
+ @ColumnInfo(name = "created_at") val createdAt: Long,
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt
new file mode 100644
index 000000000..4bd63d65d
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt
@@ -0,0 +1,23 @@
+package org.koitharu.kotatsu.bookmarks.data
+
+import androidx.room.Embedded
+import androidx.room.Junction
+import androidx.room.Relation
+import org.koitharu.kotatsu.core.db.entity.MangaEntity
+import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
+import org.koitharu.kotatsu.core.db.entity.TagEntity
+
+class BookmarkWithManga(
+ @Embedded val bookmark: BookmarkEntity,
+ @Relation(
+ parentColumn = "manga_id",
+ entityColumn = "manga_id"
+ )
+ val manga: MangaEntity,
+ @Relation(
+ parentColumn = "manga_id",
+ entityColumn = "tag_id",
+ associateBy = Junction(MangaTagsEntity::class)
+ )
+ val tags: List,
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt
new file mode 100644
index 000000000..dd023be7a
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt
@@ -0,0 +1,26 @@
+package org.koitharu.kotatsu.bookmarks.data
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+abstract class BookmarksDao {
+
+ @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
+ abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow
+
+ @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
+ abstract fun observe(mangaId: Long): Flow>
+
+ @Insert
+ abstract suspend fun insert(entity: BookmarkEntity)
+
+ @Delete
+ abstract suspend fun delete(entity: BookmarkEntity)
+
+ @Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
+ abstract suspend fun delete(mangaId: Long, pageId: Long)
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt
new file mode 100644
index 000000000..981aa05ea
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt
@@ -0,0 +1,31 @@
+package org.koitharu.kotatsu.bookmarks.data
+
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
+import org.koitharu.kotatsu.core.db.entity.toManga
+import org.koitharu.kotatsu.core.db.entity.toMangaTags
+import org.koitharu.kotatsu.parsers.model.Manga
+import java.util.*
+
+fun BookmarkWithManga.toBookmark() = bookmark.toBookmark(
+ manga.toManga(tags.toMangaTags())
+)
+
+fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
+ manga = manga,
+ pageId = pageId,
+ chapterId = chapterId,
+ page = page,
+ scroll = scroll,
+ imageUrl = imageUrl,
+ createdAt = Date(createdAt),
+)
+
+fun Bookmark.toEntity() = BookmarkEntity(
+ mangaId = manga.id,
+ pageId = pageId,
+ chapterId = chapterId,
+ page = page,
+ scroll = scroll,
+ imageUrl = imageUrl,
+ createdAt = createdAt.time,
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt
new file mode 100644
index 000000000..0b76c6537
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt
@@ -0,0 +1,43 @@
+package org.koitharu.kotatsu.bookmarks.domain
+
+import org.koitharu.kotatsu.parsers.model.Manga
+import java.util.*
+
+class Bookmark(
+ val manga: Manga,
+ val pageId: Long,
+ val chapterId: Long,
+ val page: Int,
+ val scroll: Int,
+ val imageUrl: String,
+ val createdAt: Date,
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Bookmark
+
+ if (manga != other.manga) return false
+ if (pageId != other.pageId) return false
+ if (chapterId != other.chapterId) return false
+ if (page != other.page) return false
+ if (scroll != other.scroll) return false
+ if (imageUrl != other.imageUrl) return false
+ if (createdAt != other.createdAt) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = manga.hashCode()
+ result = 31 * result + pageId.hashCode()
+ result = 31 * result + chapterId.hashCode()
+ result = 31 * result + page
+ result = 31 * result + scroll
+ result = 31 * result + imageUrl.hashCode()
+ result = 31 * result + createdAt.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt
new file mode 100644
index 000000000..df63c03aa
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt
@@ -0,0 +1,38 @@
+package org.koitharu.kotatsu.bookmarks.domain
+
+import androidx.room.withTransaction
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.koitharu.kotatsu.bookmarks.data.toBookmark
+import org.koitharu.kotatsu.bookmarks.data.toEntity
+import org.koitharu.kotatsu.core.db.MangaDatabase
+import org.koitharu.kotatsu.core.db.entity.toEntities
+import org.koitharu.kotatsu.core.db.entity.toEntity
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.utils.ext.mapItems
+
+class BookmarksRepository(
+ private val db: MangaDatabase,
+) {
+
+ fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow {
+ return db.bookmarksDao.observe(manga.id, chapterId, page).map { it?.toBookmark(manga) }
+ }
+
+ fun observeBookmarks(manga: Manga): Flow> {
+ return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) }
+ }
+
+ suspend fun addBookmark(bookmark: Bookmark) {
+ db.withTransaction {
+ val tags = bookmark.manga.tags.toEntities()
+ db.tagsDao.upsert(tags)
+ db.mangaDao.upsert(bookmark.manga.toEntity(), tags)
+ db.bookmarksDao.insert(bookmark.toEntity())
+ }
+ }
+
+ suspend fun removeBookmark(mangaId: Long, pageId: Long) {
+ db.bookmarksDao.delete(mangaId, pageId)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt
new file mode 100644
index 000000000..f8aa0e638
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt
@@ -0,0 +1,51 @@
+package org.koitharu.kotatsu.bookmarks.ui
+
+import androidx.lifecycle.LifecycleOwner
+import coil.ImageLoader
+import coil.request.Disposable
+import coil.size.Scale
+import coil.util.CoilUtils
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
+import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
+import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
+import org.koitharu.kotatsu.utils.ext.enqueueWith
+import org.koitharu.kotatsu.utils.ext.newImageRequest
+import org.koitharu.kotatsu.utils.ext.referer
+
+fun bookmarkListAD(
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+ clickListener: OnListItemClickListener,
+) = adapterDelegateViewBinding(
+ { inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) }
+) {
+
+ var imageRequest: Disposable? = null
+ val listener = AdapterDelegateClickListenerAdapter(this, clickListener)
+
+ binding.root.setOnClickListener(listener)
+ binding.root.setOnLongClickListener(listener)
+
+ bind {
+ imageRequest?.dispose()
+ imageRequest = binding.imageViewThumb.newImageRequest(item.imageUrl)
+ .referer(item.manga.publicUrl)
+ .placeholder(R.drawable.ic_placeholder)
+ .fallback(R.drawable.ic_placeholder)
+ .error(R.drawable.ic_placeholder)
+ .allowRgb565(true)
+ .scale(Scale.FILL)
+ .lifecycle(lifecycleOwner)
+ .enqueueWith(coil)
+ }
+
+ onViewRecycled {
+ imageRequest?.dispose()
+ imageRequest = null
+ CoilUtils.dispose(binding.imageViewThumb)
+ binding.imageViewThumb.setImageDrawable(null)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt
new file mode 100644
index 000000000..92040bc97
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt
@@ -0,0 +1,30 @@
+package org.koitharu.kotatsu.bookmarks.ui
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.recyclerview.widget.DiffUtil
+import coil.ImageLoader
+import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
+import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
+
+class BookmarksAdapter(
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+ clickListener: OnListItemClickListener,
+) : AsyncListDifferDelegationAdapter(
+ DiffCallback(),
+ bookmarkListAD(coil, lifecycleOwner, clickListener)
+) {
+
+ private class DiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
+ return oldItem.manga.id == newItem.manga.id && oldItem.chapterId == newItem.chapterId
+ }
+
+ override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
+ return oldItem.imageUrl == newItem.imageUrl
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt
index 3d2f668a6..4b42b4c60 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt
@@ -121,6 +121,7 @@ class BackupRepository(private val db: MangaDatabase) {
jo.put("sort_key", sortKey)
jo.put("title", title)
jo.put("order", order)
+ jo.put("track", track)
return jo
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt
index ba4a8b6a3..57fd4d6ee 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt
@@ -104,6 +104,7 @@ class RestoreRepository(private val db: MangaDatabase) {
sortKey = json.getInt("sort_key"),
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
+ track = json.getBooleanOrDefault("track", true),
)
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt
index dadbb05eb..215d02259 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt
@@ -5,5 +5,5 @@ import org.koin.dsl.module
val databaseModule
get() = module {
- single { MangaDatabase.create(androidContext()) }
+ single { MangaDatabase(androidContext()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt
index 35c52192c..6257a8456 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt
@@ -10,8 +10,8 @@ class DatabasePrePopulateCallback(private val resources: Resources) : RoomDataba
override fun onCreate(db: SupportSQLiteDatabase) {
db.execSQL(
- "INSERT INTO favourite_categories (created_at, sort_key, title, `order`) VALUES (?,?,?,?)",
- arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name)
+ "INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track) VALUES (?,?,?,?,?)",
+ arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name, 1)
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt
index a4e5812d9..0714c0fcc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt
@@ -4,6 +4,8 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
+import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
+import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
import org.koitharu.kotatsu.core.db.dao.*
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.db.migrations.*
@@ -20,9 +22,9 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
- TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
+ TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
],
- version = 9
+ version = 11,
)
abstract class MangaDatabase : RoomDatabase() {
@@ -44,30 +46,24 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val suggestionDao: SuggestionDao
- companion object {
+ abstract val bookmarksDao: BookmarksDao
+}
- const val TABLE_FAVOURITES = "favourites"
- const val TABLE_MANGA = "manga"
- const val TABLE_TAGS = "tags"
- const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
- const val TABLE_HISTORY = "history"
- const val TABLE_MANGA_TAGS = "manga_tags"
-
- fun create(context: Context): MangaDatabase = Room.databaseBuilder(
- context,
- MangaDatabase::class.java,
- "kotatsu-db"
- ).addMigrations(
- Migration1To2(),
- Migration2To3(),
- Migration3To4(),
- Migration4To5(),
- Migration5To6(),
- Migration6To7(),
- Migration7To8(),
- Migration8To9(),
- ).addCallback(
- DatabasePrePopulateCallback(context.resources)
- ).build()
- }
-}
\ No newline at end of file
+fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder(
+ context,
+ MangaDatabase::class.java,
+ "kotatsu-db"
+).addMigrations(
+ Migration1To2(),
+ Migration2To3(),
+ Migration3To4(),
+ Migration4To5(),
+ Migration5To6(),
+ Migration6To7(),
+ Migration7To8(),
+ Migration8To9(),
+ Migration9To10(),
+ Migration10To11(),
+).addCallback(
+ DatabasePrePopulateCallback(context.resources)
+).build()
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt
new file mode 100644
index 000000000..1e47f1e9a
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt
@@ -0,0 +1,8 @@
+package org.koitharu.kotatsu.core.db
+
+const val TABLE_FAVOURITES = "favourites"
+const val TABLE_MANGA = "manga"
+const val TABLE_TAGS = "tags"
+const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
+const val TABLE_HISTORY = "history"
+const val TABLE_MANGA_TAGS = "manga_tags"
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt
index 4bd188966..f8352524b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt
@@ -10,6 +10,9 @@ abstract class TracksDao {
@Query("SELECT * FROM tracks")
abstract suspend fun findAll(): List
+ @Query("SELECT * FROM tracks WHERE manga_id IN (:ids)")
+ abstract suspend fun findAll(ids: Collection): List
+
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun find(mangaId: Long): TrackEntity?
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt
index 1447e914d..d3b64295a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA
+import org.koitharu.kotatsu.core.db.TABLE_MANGA
@Entity(tableName = TABLE_MANGA)
class MangaEntity(
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt
index ef13b2824..bc343f784 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA_TAGS
+import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
@Entity(
tableName = TABLE_MANGA_TAGS,
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt
index 368b675df..23f61d6d8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_TAGS
+import org.koitharu.kotatsu.core.db.TABLE_TAGS
@Entity(tableName = TABLE_TAGS)
class TagEntity(
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt
new file mode 100644
index 000000000..5d80708fe
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt
@@ -0,0 +1,26 @@
+package org.koitharu.kotatsu.core.db.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+class Migration10To11 : Migration(10, 11) {
+
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(
+ """
+ CREATE TABLE IF NOT EXISTS `bookmarks` (
+ `manga_id` INTEGER NOT NULL,
+ `page_id` INTEGER NOT NULL,
+ `chapter_id` INTEGER NOT NULL,
+ `page` INTEGER NOT NULL,
+ `scroll` INTEGER NOT NULL,
+ `image` TEXT NOT NULL,
+ `created_at` INTEGER NOT NULL,
+ PRIMARY KEY(`manga_id`, `page_id`),
+ FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )
+ """.trimIndent()
+ )
+ database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `bookmarks` (`manga_id`)")
+ database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `bookmarks` (`page_id`)")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt
new file mode 100644
index 000000000..59cba96ef
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt
@@ -0,0 +1,11 @@
+package org.koitharu.kotatsu.core.db.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+class Migration9To10 : Migration(9, 10) {
+
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt
index de5256337..58d8d22c6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt
@@ -4,7 +4,5 @@ import org.koin.dsl.module
val githubModule
get() = module {
- single {
- GithubRepository(get())
- }
+ factory { GithubRepository(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
index 09557cb47..88304755b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
@@ -54,27 +54,23 @@ class VersionId(
return result
}
- companion object {
-
- private fun variantWeight(variantType: String) =
- when (variantType.lowercase(Locale.ROOT)) {
- "a", "alpha" -> 1
- "b", "beta" -> 2
- "rc" -> 4
- "" -> 8
- else -> 0
- }
-
- fun parse(versionName: String): VersionId {
- val parts = versionName.substringBeforeLast('-').split('.')
- val variant = versionName.substringAfterLast('-', "")
- return VersionId(
- major = parts.getOrNull(0)?.toIntOrNull() ?: 0,
- minor = parts.getOrNull(1)?.toIntOrNull() ?: 0,
- build = parts.getOrNull(2)?.toIntOrNull() ?: 0,
- variantType = variant.filter(Char::isLetter),
- variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0
- )
- }
+ private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
+ "a", "alpha" -> 1
+ "b", "beta" -> 2
+ "rc" -> 4
+ "" -> 8
+ else -> 0
}
+}
+
+fun VersionId(versionName: String): VersionId {
+ val parts = versionName.substringBeforeLast('-').split('.')
+ val variant = versionName.substringAfterLast('-', "")
+ return VersionId(
+ major = parts.getOrNull(0)?.toIntOrNull() ?: 0,
+ minor = parts.getOrNull(1)?.toIntOrNull() ?: 0,
+ build = parts.getOrNull(2)?.toIntOrNull() ?: 0,
+ variantType = variant.filter(Char::isLetter),
+ variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0,
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt
index 655a0eb08..798ec2fbd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt
@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
+import java.util.*
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.SortOrder
-import java.util.*
@Parcelize
data class FavouriteCategory(
@@ -12,4 +12,5 @@ data class FavouriteCategory(
val sortKey: Int,
val order: SortOrder,
val createdAt: Date,
+ val isTrackingEnabled: Boolean,
) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/DoHManager.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/DoHManager.kt
new file mode 100644
index 000000000..7c3c2db6e
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/DoHManager.kt
@@ -0,0 +1,84 @@
+package org.koitharu.kotatsu.core.network
+
+import okhttp3.Cache
+import okhttp3.Dns
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.dnsoverhttps.DnsOverHttps
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+import java.net.InetAddress
+import java.net.UnknownHostException
+
+class DoHManager(
+ cache: Cache,
+ private val settings: AppSettings,
+) : Dns {
+
+ private val bootstrapClient = OkHttpClient.Builder().cache(cache).build()
+
+ private var cachedDelegate: Dns? = null
+ private var cachedProvider: DoHProvider? = null
+
+ override fun lookup(hostname: String): List {
+ return getDelegate().lookup(hostname)
+ }
+
+ @Synchronized
+ private fun getDelegate(): Dns {
+ var delegate = cachedDelegate
+ val provider = settings.dnsOverHttps
+ if (delegate == null || provider != cachedProvider) {
+ delegate = createDelegate(provider)
+ cachedDelegate = delegate
+ cachedProvider = provider
+ }
+ return delegate
+ }
+
+ private fun createDelegate(provider: DoHProvider): Dns = when (provider) {
+ DoHProvider.NONE -> Dns.SYSTEM
+ DoHProvider.GOOGLE -> DnsOverHttps.Builder().client(bootstrapClient)
+ .url("https://dns.google/dns-query".toHttpUrl())
+ .bootstrapDnsHosts(
+ listOfNotNull(
+ tryGetByIp("8.8.4.4"),
+ tryGetByIp("8.8.8.8"),
+ tryGetByIp("2001:4860:4860::8888"),
+ tryGetByIp("2001:4860:4860::8844"),
+ )
+ ).build()
+ DoHProvider.CLOUDFLARE -> DnsOverHttps.Builder().client(bootstrapClient)
+ .url("https://cloudflare-dns.com/dns-query".toHttpUrl())
+ .bootstrapDnsHosts(
+ listOfNotNull(
+ tryGetByIp("162.159.36.1"),
+ tryGetByIp("162.159.46.1"),
+ tryGetByIp("1.1.1.1"),
+ tryGetByIp("1.0.0.1"),
+ tryGetByIp("162.159.132.53"),
+ tryGetByIp("2606:4700:4700::1111"),
+ tryGetByIp("2606:4700:4700::1001"),
+ tryGetByIp("2606:4700:4700::0064"),
+ tryGetByIp("2606:4700:4700::6400"),
+ )
+ ).build()
+ DoHProvider.ADGUARD -> DnsOverHttps.Builder().client(bootstrapClient)
+ .url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl())
+ .bootstrapDnsHosts(
+ listOfNotNull(
+ tryGetByIp("94.140.14.140"),
+ tryGetByIp("94.140.14.141"),
+ tryGetByIp("2a10:50c0::1:ff"),
+ tryGetByIp("2a10:50c0::2:ff"),
+ )
+ ).build()
+ }
+
+ private fun tryGetByIp(ip: String): InetAddress? = try {
+ InetAddress.getByName(ip)
+ } catch (e: UnknownHostException) {
+ e.printStackTraceDebug()
+ null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/DoHProvider.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/DoHProvider.kt
new file mode 100644
index 000000000..e17db70a7
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/DoHProvider.kt
@@ -0,0 +1,6 @@
+package org.koitharu.kotatsu.core.network
+
+enum class DoHProvider {
+
+ NONE, GOOGLE, CLOUDFLARE, ADGUARD
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt
index 48b009a33..2af4c215e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt
@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.network
-import java.util.concurrent.TimeUnit
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koin.dsl.bind
@@ -8,17 +7,20 @@ import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.MangaLoaderContext
+import java.util.concurrent.TimeUnit
val networkModule
get() = module {
single { AndroidCookieJar() } bind CookieJar::class
single {
+ val cache = get().createHttpCache()
OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(get())
- cache(get().createHttpCache())
+ dns(DoHManager(cache, get()))
+ cache(cache)
addInterceptor(UserAgentInterceptor())
addInterceptor(CloudFlareInterceptor())
}.build()
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt
index 98ebe3a6a..9e8b18a52 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt
@@ -5,12 +5,12 @@ import android.content.Context
import android.content.pm.ShortcutManager
import android.media.ThumbnailUtils
import android.os.Build
+import android.util.Size
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader
import coil.request.ImageRequest
-import coil.size.PixelSize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
@@ -54,7 +54,7 @@ class ShortcutsRepository(
val bmp = coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
- .size(iconSize)
+ .size(iconSize.width, iconSize.height)
.build()
).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
@@ -74,14 +74,14 @@ class ShortcutsRepository(
)
}
- private fun getIconSize(context: Context): PixelSize {
+ private fun getIconSize(context: Context): Size {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
(context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let {
- PixelSize(it.iconMaxWidth, it.iconMaxHeight)
+ Size(it.iconMaxWidth, it.iconMaxHeight)
}
} else {
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let {
- PixelSize(it, it)
+ Size(it, it)
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt
index 4a386d3f8..ba5412a50 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt
@@ -2,17 +2,19 @@ package org.koitharu.kotatsu.core.parser
import android.net.Uri
import coil.map.Mapper
+import coil.request.Options
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.parsers.model.MangaSource
-class FaviconMapper() : Mapper {
+class FaviconMapper : Mapper {
- override fun map(data: Uri): HttpUrl {
+ override fun map(data: Uri, options: Options): HttpUrl? {
+ if (data.scheme != "favicon") {
+ return null
+ }
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
return repo.getFaviconUrl().toHttpUrl()
}
-
- override fun handles(data: Uri) = data.scheme == "favicon"
}
\ No newline at end of file
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 d9178713c..b81fcbe7a 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
@@ -16,6 +16,7 @@ import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.ZoomMode
+import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.putEnumValue
@@ -78,7 +79,10 @@ class AppSettings(context: Context) {
get() = prefs.getLong(KEY_APP_UPDATE, 0L)
set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) }
- val trackerNotifications: Boolean
+ val isTrackerEnabled: Boolean
+ get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
+
+ val isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
var notificationSound: Uri
@@ -95,8 +99,11 @@ class AppSettings(context: Context) {
val readerAnimation: Boolean
get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
- val isPreferRtlReader: Boolean
- get() = prefs.getBoolean(KEY_READER_PREFER_RTL, false)
+ val defaultReaderMode: ReaderMode
+ get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
+
+ val isReaderModeDetectionEnabled: Boolean
+ get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true)
var historyGrouping: Boolean
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
@@ -185,6 +192,9 @@ class AppSettings(context: Context) {
get() = prefs.getBoolean(KEY_SEARCH_SINGLE_SOURCE, false)
set(value) = prefs.edit { putBoolean(KEY_SEARCH_SINGLE_SOURCE, value) }
+ val dnsOverHttps: DoHProvider
+ get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
+
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
NETWORK_ALWAYS -> true
@@ -269,15 +279,19 @@ class AppSettings(context: Context) {
const val KEY_REMOTE_SOURCES = "remote_sources"
const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_SWITCHERS = "reader_switchers"
+ const val KEY_TRACKER_ENABLED = "tracker_enabled"
const val KEY_TRACK_SOURCES = "track_sources"
+ const val KEY_TRACK_CATEGORIES = "track_categories"
const val KEY_TRACK_WARNING = "track_warning"
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
+ const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
const val KEY_READER_ANIMATION = "reader_animation"
- const val KEY_READER_PREFER_RTL = "reader_prefer_rtl"
+ const val KEY_READER_MODE = "reader_mode"
+ const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password"
const val KEY_PROTECT_APP = "protect_app"
const val KEY_APP_VERSION = "app_version"
@@ -297,6 +311,7 @@ class AppSettings(context: Context) {
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
+ const val KEY_DOH = "doh"
// About
const val KEY_APP_UPDATE = "app_update"
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt
new file mode 100644
index 000000000..88c62514c
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt
@@ -0,0 +1,35 @@
+package org.koitharu.kotatsu.core.prefs
+
+import androidx.lifecycle.liveData
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.flow
+
+fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
+ var lastValue: T = valueProducer()
+ emit(lastValue)
+ observe().collect {
+ if (it == key) {
+ val value = valueProducer()
+ if (value != lastValue) {
+ emit(value)
+ }
+ lastValue = value
+ }
+ }
+}
+
+fun AppSettings.observeAsLiveData(
+ context: CoroutineContext,
+ key: String,
+ valueProducer: AppSettings.() -> T
+) = liveData(context) {
+ emit(valueProducer())
+ observe().collect {
+ if (it == key) {
+ val value = valueProducer()
+ if (value != latestValue) {
+ emit(value)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt
index bfc8b7b83..9ec51d479 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt
@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt
index 20a7bf0c3..fb3216cb2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import kotlin.system.exitProcess
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler {
@@ -13,7 +14,7 @@ class AppCrashHandler(private val applicationContext: Context) : Thread.Uncaught
try {
applicationContext.startActivity(intent)
} catch (t: Throwable) {
- t.printStackTrace()
+ t.printStackTraceDebug()
}
Log.e("CRASH", e.message, e)
exitProcess(1)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
index 26ccbf44a..54529b37c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.core.ui
import coil.ComponentRegistry
import coil.ImageLoader
-import coil.util.CoilUtils
+import coil.disk.DiskCache
+import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.FaviconMapper
+import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule
@@ -14,15 +16,23 @@ val uiModule
single {
val httpClientFactory = {
get().newBuilder()
- .cache(CoilUtils.createDefaultCache(androidContext()))
+ .cache(null)
+ .build()
+ }
+ val diskCacheFactory = {
+ val context = androidContext()
+ val rootDir = context.externalCacheDir ?: context.cacheDir
+ DiskCache.Builder()
+ .directory(rootDir.resolve(CacheDir.THUMBS.dir))
.build()
}
ImageLoader.Builder(androidContext())
.okHttpClient(httpClientFactory)
- .launchInterceptorChainOnMainThread(false)
- .componentRegistry(
+ .interceptorDispatcher(Dispatchers.Default)
+ .diskCache(diskCacheFactory)
+ .components(
ComponentRegistry.Builder()
- .add(CbzFetcher())
+ .add(CbzFetcher.Factory())
.add(FaviconMapper())
.build()
).build()
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt
index 7e3bd8622..916b75de1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt
@@ -8,6 +8,6 @@ val detailsModule
get() = module {
viewModel { intent ->
- DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get())
+ DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get(), get())
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt b/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt
new file mode 100644
index 000000000..d0b5a23c3
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt
@@ -0,0 +1,6 @@
+package org.koitharu.kotatsu.details.domain
+
+class BranchComparator : Comparator {
+
+ override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2)
+}
\ No newline at end of file
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 a1920bf80..f6ecc6f0d 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
@@ -83,6 +83,9 @@ class DetailsActivity :
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError)
+ viewModel.onShowToast.observe(this) {
+ binding.snackbar.show(messageText = getString(it), longDuration = false)
+ }
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
}
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 df4fc2de9..51d320e00 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
@@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.text.parseAsHtml
+import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import coil.ImageLoader
@@ -21,10 +22,14 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
+import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
+import org.koitharu.kotatsu.bookmarks.ui.BookmarksAdapter
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
-import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
+import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -41,7 +46,8 @@ class DetailsFragment :
BaseFragment(),
View.OnClickListener,
View.OnLongClickListener,
- ChipsView.OnChipClickListener {
+ ChipsView.OnChipClickListener,
+ OnListItemClickListener {
private val viewModel by sharedViewModel()
private val coil by inject(mode = LazyThreadSafetyMode.NONE)
@@ -69,6 +75,7 @@ class DetailsFragment :
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
+ viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -76,6 +83,24 @@ class DetailsFragment :
inflater.inflate(R.menu.opt_details_info, menu)
}
+ override fun onItemClick(item: Bookmark, view: View) {
+ val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.measuredWidth, view.measuredHeight)
+ startActivity(ReaderActivity.newIntent(view.context, item), options.toBundle())
+ }
+
+ override fun onItemLongClick(item: Bookmark, view: View): Boolean {
+ val menu = PopupMenu(view.context, view)
+ menu.inflate(R.menu.popup_bookmark)
+ menu.setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ R.id.action_remove -> viewModel.removeBookmark(item)
+ }
+ true
+ }
+ menu.show()
+ return true
+ }
+
private fun onMangaUpdated(manga: Manga) {
with(binding) {
// Main
@@ -176,11 +201,25 @@ class DetailsFragment :
}
}
+ private fun onBookmarksChanged(bookmarks: List) {
+ var adapter = binding.recyclerViewBookmarks.adapter as? BookmarksAdapter
+ binding.groupBookmarks.isGone = bookmarks.isEmpty()
+ if (adapter != null) {
+ adapter.items = bookmarks
+ } else {
+ adapter = BookmarksAdapter(coil, viewLifecycleOwner, this)
+ adapter.items = bookmarks
+ binding.recyclerViewBookmarks.adapter = adapter
+ val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing)
+ binding.recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing))
+ }
+ }
+
override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return
when (v.id) {
R.id.button_favorite -> {
- FavouriteCategoriesDialog.show(childFragmentManager, manga)
+ FavouriteCategoriesBottomSheet.show(childFragmentManager, manga)
}
R.id.button_read -> {
val chapterId = viewModel.readingHistory.value?.chapterId
@@ -283,7 +322,7 @@ class DetailsFragment :
.target(binding.imageViewCover)
if (currentCover != null) {
request.data(manga.largeCoverUrl ?: return)
- .placeholderMemoryCacheKey(CoilUtils.metadata(binding.imageViewCover)?.memoryCacheKey)
+ .placeholderMemoryCacheKey(CoilUtils.result(binding.imageViewCover)?.request?.memoryCacheKey)
.fallback(currentCover)
} else {
request.crossfade(true)
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index 0ee363efd..382899c3a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -1,121 +1,104 @@
package org.koitharu.kotatsu.details.ui
-import androidx.core.os.LocaleListCompat
-import androidx.lifecycle.asFlow
-import androidx.lifecycle.asLiveData
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
+import androidx.lifecycle.*
+import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
-import org.koitharu.kotatsu.BuildConfig
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseViewModel
-import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
-import org.koitharu.kotatsu.core.parser.MangaRepository
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
+import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
-import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
-import org.koitharu.kotatsu.parsers.util.toTitleCase
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.ext.iterator
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.IOException
class DetailsViewModel(
- private val intent: MangaIntent,
+ intent: MangaIntent,
private val historyRepository: HistoryRepository,
- private val favouritesRepository: FavouritesRepository,
+ favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository,
- private val mangaDataRepository: MangaDataRepository,
+ mangaDataRepository: MangaDataRepository,
+ private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings,
) : BaseViewModel() {
+ private val delegate = MangaDetailsDelegate(
+ intent = intent,
+ settings = settings,
+ mangaDataRepository = mangaDataRepository,
+ historyRepository = historyRepository,
+ localMangaRepository = localMangaRepository,
+ )
+
private var loadingJob: Job
- private val mangaData = MutableStateFlow(intent.manga)
- private val selectedBranch = MutableStateFlow(null)
- private val history = mangaData.mapNotNull { it?.id }
- .distinctUntilChanged()
- .flatMapLatest { mangaId ->
- historyRepository.observeOne(mangaId)
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
+ val onShowToast = SingleLiveEvent()
- private val favourite = mangaData.mapNotNull { it?.id }
- .distinctUntilChanged()
- .flatMapLatest { mangaId ->
- favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
+ private val history = historyRepository.observeOne(delegate.mangaId)
+ .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
- private val newChapters = mangaData.mapNotNull { it?.id }
- .distinctUntilChanged()
- .mapLatest { mangaId ->
- trackingRepository.getNewChaptersCount(mangaId)
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
-
- // Remote manga for saved and saved for remote
- private val relatedManga = MutableStateFlow(null)
- private val chaptersQuery = MutableStateFlow("")
-
- private val chaptersReversed = settings.observe()
- .filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
- .map { settings.chaptersReverse }
- .onStart { emit(settings.chaptersReverse) }
+ private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
- val manga = mangaData.filterNotNull()
- .asLiveData(viewModelScope.coroutineContext)
- val favouriteCategories = favourite
- .asLiveData(viewModelScope.coroutineContext)
- val newChaptersCount = newChapters
- .asLiveData(viewModelScope.coroutineContext)
- val readingHistory = history
- .asLiveData(viewModelScope.coroutineContext)
- val isChaptersReversed = chaptersReversed
- .asLiveData(viewModelScope.coroutineContext)
+ private val newChapters = viewModelScope.async(Dispatchers.Default) {
+ trackingRepository.getNewChaptersCount(delegate.mangaId)
+ }
+
+ private val chaptersQuery = MutableStateFlow("")
+
+ private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
+ .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
+
+ val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
+ val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
+ val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) }
+ val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
+ val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
+
+ val bookmarks = delegate.manga.flatMapLatest {
+ if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
+ }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val onMangaRemoved = SingleLiveEvent()
- val branches = mangaData.map {
- it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
+ val branches: LiveData> = delegate.manga.map {
+ val chapters = it?.chapters ?: return@map emptyList()
+ chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchIndex = combine(
branches.asFlow(),
- selectedBranch
+ delegate.selectedBranch
) { branches, selected ->
branches.indexOf(selected)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
- val isChaptersEmpty = mangaData.mapNotNull { m ->
- m?.run { chapters.isNullOrEmpty() }
+ val isChaptersEmpty: LiveData = delegate.manga.map { m ->
+ m != null && m.chapters.isNullOrEmpty()
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
val chapters = combine(
combine(
- mangaData.map { it?.chapters.orEmpty() },
- relatedManga,
- history.map { it?.chapterId },
- newChapters,
- selectedBranch
- ) { chapters, related, currentId, newCount, branch ->
- val relatedChapters = related?.chapters
- if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
- mapChaptersWithSource(chapters, relatedChapters, currentId, newCount, branch)
- } else {
- mapChapters(chapters, relatedChapters, currentId, newCount, branch)
- }
+ delegate.manga,
+ delegate.relatedManga,
+ history,
+ delegate.selectedBranch,
+ ) { manga, related, history, branch ->
+ delegate.mapChapters(manga, related, history, newChapters.await(), branch)
},
chaptersReversed,
chaptersQuery,
@@ -124,7 +107,7 @@ class DetailsViewModel(
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchValue: String?
- get() = selectedBranch.value
+ get() = delegate.selectedBranch.value
init {
loadingJob = doLoad()
@@ -136,7 +119,11 @@ class DetailsViewModel(
}
fun deleteLocal() {
- val m = mangaData.value ?: return
+ val m = delegate.manga.value
+ if (m == null) {
+ onShowToast.call(R.string.file_not_found)
+ return
+ }
launchLoadingJob(Dispatchers.Default) {
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
@@ -149,16 +136,23 @@ class DetailsViewModel(
}
}
+ fun removeBookmark(bookmark: Bookmark) {
+ launchJob {
+ bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId)
+ onShowToast.call(R.string.bookmark_removed)
+ }
+ }
+
fun setChaptersReversed(newValue: Boolean) {
settings.chaptersReverse = newValue
}
fun setSelectedBranch(branch: String?) {
- selectedBranch.value = branch
+ delegate.selectedBranch.value = branch
}
fun getRemoteManga(): Manga? {
- return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
+ return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
}
fun performChapterSearch(query: String?) {
@@ -166,7 +160,7 @@ class DetailsViewModel(
}
fun onDownloadComplete(downloadedManga: Manga) {
- val currentManga = mangaData.value ?: return
+ val currentManga = delegate.manga.value ?: return
if (currentManga.id != downloadedManga.id) {
return
}
@@ -177,142 +171,16 @@ class DetailsViewModel(
runCatching {
localMangaRepository.getDetails(downloadedManga)
}.onSuccess {
- relatedManga.value = it
+ delegate.relatedManga.value = it
}.onFailure {
- if (BuildConfig.DEBUG) {
- it.printStackTrace()
- }
+ it.printStackTraceDebug()
}
}
}
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
- var manga = mangaDataRepository.resolveIntent(intent)
- ?: throw MangaNotFoundException("Cannot find manga")
- mangaData.value = manga
- manga = MangaRepository(manga.source).getDetails(manga)
- // find default branch
- val hist = historyRepository.getOne(manga)
- selectedBranch.value = if (hist != null) {
- val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
- if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
- } else {
- predictBranch(manga.chapters)
- }
- mangaData.value = manga
- relatedManga.value = runCatching {
- if (manga.source == MangaSource.LOCAL) {
- val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
- MangaRepository(m.source).getDetails(m)
- } else {
- localMangaRepository.findSavedManga(manga)
- }
- }.onFailure { error ->
- if (BuildConfig.DEBUG) error.printStackTrace()
- }.getOrNull()
- }
-
- private fun mapChapters(
- chapters: List,
- downloadedChapters: List?,
- currentId: Long?,
- newCount: Int,
- branch: String?,
- ): List {
- val result = ArrayList(chapters.size)
- val dateFormat = settings.getDateFormat()
- val currentIndex = chapters.indexOfFirst { it.id == currentId }
- val firstNewIndex = chapters.size - newCount
- val downloadedIds = downloadedChapters?.mapToSet { it.id }
- for (i in chapters.indices) {
- val chapter = chapters[i]
- if (chapter.branch != branch) {
- continue
- }
- result += chapter.toListItem(
- isCurrent = i == currentIndex,
- isUnread = i > currentIndex,
- isNew = i >= firstNewIndex,
- isMissing = false,
- isDownloaded = downloadedIds?.contains(chapter.id) == true,
- dateFormat = dateFormat,
- )
- }
- return result
- }
-
- private fun mapChaptersWithSource(
- chapters: List,
- sourceChapters: List,
- currentId: Long?,
- newCount: Int,
- branch: String?,
- ): List {
- val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
- val result = ArrayList(sourceChapters.size)
- val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
- val firstNewIndex = sourceChapters.size - newCount
- val dateFormat = settings.getDateFormat()
- for (i in sourceChapters.indices) {
- val chapter = sourceChapters[i]
- val localChapter = chaptersMap.remove(chapter.id)
- if (chapter.branch != branch) {
- continue
- }
- result += localChapter?.toListItem(
- isCurrent = i == currentIndex,
- isUnread = i > currentIndex,
- isNew = i >= firstNewIndex,
- isMissing = false,
- isDownloaded = false,
- dateFormat = dateFormat,
- ) ?: chapter.toListItem(
- isCurrent = i == currentIndex,
- isUnread = i > currentIndex,
- isNew = i >= firstNewIndex,
- isMissing = true,
- isDownloaded = false,
- dateFormat = dateFormat,
- )
- }
- if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
- result.ensureCapacity(result.size + chaptersMap.size)
- chaptersMap.values.mapNotNullTo(result) {
- if (it.branch == branch) {
- it.toListItem(
- isCurrent = false,
- isUnread = true,
- isNew = false,
- isMissing = false,
- isDownloaded = false,
- dateFormat = dateFormat,
- )
- } else {
- null
- }
- }
- result.sortBy { it.chapter.number }
- }
- return result
- }
-
- private fun predictBranch(chapters: List?): String? {
- if (chapters.isNullOrEmpty()) {
- return null
- }
- val groups = chapters.groupBy { it.branch }
- for (locale in LocaleListCompat.getAdjustedDefault()) {
- var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
- if (groups.containsKey(language)) {
- return language
- }
- language = locale.getDisplayName(locale).toTitleCase(locale)
- if (groups.containsKey(language)) {
- return language
- }
- }
- return groups.maxByOrNull { it.value.size }?.key
+ delegate.doLoad()
}
private fun List.filterSearch(query: String): List {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
new file mode 100644
index 000000000..07f03dbda
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
@@ -0,0 +1,184 @@
+package org.koitharu.kotatsu.details.ui
+
+import androidx.core.os.LocaleListCompat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.koitharu.kotatsu.base.domain.MangaDataRepository
+import org.koitharu.kotatsu.base.domain.MangaIntent
+import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
+import org.koitharu.kotatsu.core.model.MangaHistory
+import org.koitharu.kotatsu.core.parser.MangaRepository
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem
+import org.koitharu.kotatsu.details.ui.model.toListItem
+import org.koitharu.kotatsu.history.domain.HistoryRepository
+import org.koitharu.kotatsu.local.domain.LocalMangaRepository
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.util.mapToSet
+import org.koitharu.kotatsu.parsers.util.toTitleCase
+import org.koitharu.kotatsu.utils.ext.iterator
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+
+class MangaDetailsDelegate(
+ private val intent: MangaIntent,
+ private val settings: AppSettings,
+ private val mangaDataRepository: MangaDataRepository,
+ private val historyRepository: HistoryRepository,
+ private val localMangaRepository: LocalMangaRepository,
+) {
+
+ private val mangaData = MutableStateFlow(intent.manga)
+
+ val selectedBranch = MutableStateFlow(null)
+ // Remote manga for saved and saved for remote
+ val relatedManga = MutableStateFlow(null)
+ val manga: StateFlow
+ get() = mangaData
+ val mangaId = intent.manga?.id ?: intent.mangaId
+
+ suspend fun doLoad() {
+ var manga = mangaDataRepository.resolveIntent(intent)
+ ?: throw MangaNotFoundException("Cannot find manga")
+ mangaData.value = manga
+ manga = MangaRepository(manga.source).getDetails(manga)
+ // find default branch
+ val hist = historyRepository.getOne(manga)
+ selectedBranch.value = if (hist != null) {
+ val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
+ if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
+ } else {
+ predictBranch(manga.chapters)
+ }
+ mangaData.value = manga
+ relatedManga.value = runCatching {
+ if (manga.source == MangaSource.LOCAL) {
+ val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
+ MangaRepository(m.source).getDetails(m)
+ } else {
+ localMangaRepository.findSavedManga(manga)
+ }
+ }.onFailure { error ->
+ error.printStackTraceDebug()
+ }.getOrNull()
+ }
+
+ fun mapChapters(
+ manga: Manga?,
+ related: Manga?,
+ history: MangaHistory?,
+ newCount: Int,
+ branch: String?,
+ ): List {
+ val chapters = manga?.chapters ?: return emptyList()
+ val relatedChapters = related?.chapters
+ return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
+ mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
+ } else {
+ mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
+ }
+ }
+
+ private fun mapChapters(
+ chapters: List,
+ downloadedChapters: List?,
+ currentId: Long?,
+ newCount: Int,
+ branch: String?,
+ ): List {
+ val result = ArrayList(chapters.size)
+ val dateFormat = settings.getDateFormat()
+ val currentIndex = chapters.indexOfFirst { it.id == currentId }
+ val firstNewIndex = chapters.size - newCount
+ val downloadedIds = downloadedChapters?.mapToSet { it.id }
+ for (i in chapters.indices) {
+ val chapter = chapters[i]
+ if (chapter.branch != branch) {
+ continue
+ }
+ result += chapter.toListItem(
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = false,
+ isDownloaded = downloadedIds?.contains(chapter.id) == true,
+ dateFormat = dateFormat,
+ )
+ }
+ return result
+ }
+
+ private fun mapChaptersWithSource(
+ chapters: List,
+ sourceChapters: List,
+ currentId: Long?,
+ newCount: Int,
+ branch: String?,
+ ): List {
+ val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
+ val result = ArrayList(sourceChapters.size)
+ val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
+ val firstNewIndex = sourceChapters.size - newCount
+ val dateFormat = settings.getDateFormat()
+ for (i in sourceChapters.indices) {
+ val chapter = sourceChapters[i]
+ val localChapter = chaptersMap.remove(chapter.id)
+ if (chapter.branch != branch) {
+ continue
+ }
+ result += localChapter?.toListItem(
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ ) ?: chapter.toListItem(
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = true,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ )
+ }
+ if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
+ result.ensureCapacity(result.size + chaptersMap.size)
+ chaptersMap.values.mapNotNullTo(result) {
+ if (it.branch == branch) {
+ it.toListItem(
+ isCurrent = false,
+ isUnread = true,
+ isNew = false,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ )
+ } else {
+ null
+ }
+ }
+ result.sortBy { it.chapter.number }
+ }
+ return result
+ }
+
+ private fun predictBranch(chapters: List?): String? {
+ if (chapters.isNullOrEmpty()) {
+ return null
+ }
+ val groups = chapters.groupBy { it.branch }
+ for (locale in LocaleListCompat.getAdjustedDefault()) {
+ var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
+ if (groups.containsKey(language)) {
+ return language
+ }
+ language = locale.getDisplayName(locale).toTitleCase(locale)
+ if (groups.containsKey(language)) {
+ return language
+ }
+ }
+ return groups.maxByOrNull { it.value.size }?.key
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt
index 9a423b2e2..f65951d47 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt
@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.details.ui.adapter
-import android.view.View
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -21,11 +21,7 @@ fun chapterListItemAD(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
) {
- val eventListener = object : View.OnClickListener, View.OnLongClickListener {
- override fun onClick(v: View) = clickListener.onItemClick(item, v)
- override fun onLongClick(v: View) = clickListener.onItemLongClick(item, v)
- }
-
+ val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
index 58335ed31..d079eb51f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
@@ -12,7 +12,6 @@ import kotlinx.coroutines.sync.Semaphore
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.IOException
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -24,6 +23,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.waitForNetwork
import org.koitharu.kotatsu.utils.progress.ProgressJob
@@ -156,9 +156,7 @@ class DownloadManager(
outState.value = DownloadState.Cancelled(startId, manga, cover)
throw e
} catch (e: Throwable) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
outState.value = DownloadState.Error(startId, manga, cover, e)
} finally {
withContext(NonCancellable) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
index e249e4dc5..a0c6c63dd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
@@ -3,10 +3,8 @@ package org.koitharu.kotatsu.download.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
-import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.flatMapLatest
@@ -17,7 +15,7 @@ import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService
-import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection
+import org.koitharu.kotatsu.utils.bindServiceWithLifecycle
class DownloadsActivity : BaseActivity() {
@@ -28,11 +26,10 @@ class DownloadsActivity : BaseActivity() {
val adapter = DownloadsAdapter(lifecycleScope, get())
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
- LifecycleAwareServiceConnection.bindService(
- this,
- this,
- Intent(this, DownloadService::class.java),
- 0
+ bindServiceWithLifecycle(
+ owner = this,
+ service = Intent(this, DownloadService::class.java),
+ flags = 0,
).service.flatMapLatest { binder ->
(binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null)
}.onEach {
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
index 528908bfb..a424e7086 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
@@ -59,6 +59,13 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setStyle(null)
builder.setLargeIcon(state.cover?.toBitmap())
builder.clearActions()
+ builder.setVisibility(
+ if (state.manga.isNsfw) {
+ NotificationCompat.VISIBILITY_PRIVATE
+ } else {
+ NotificationCompat.VISIBILITY_PUBLIC
+ }
+ )
when (state) {
is DownloadState.Cancelled -> {
builder.setProgress(1, 0, true)
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
index 05c6df6bd..91c742e73 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
@@ -99,39 +99,42 @@ class DownloadService : BaseService() {
private fun listenJob(job: ProgressJob) {
lifecycleScope.launch {
val startId = job.progressValue.startId
- val timeLeftEstimator = TimeLeftEstimator()
val notification = DownloadNotification(this@DownloadService, startId)
- notificationSwitcher.notify(startId, notification.create(job.progressValue, -1L))
- job.progressAsFlow()
- .onEach { state ->
- if (state is DownloadState.Progress) {
- timeLeftEstimator.tick(value = state.progress, total = state.max)
- } else {
- timeLeftEstimator.emptyTick()
+ try {
+ val timeLeftEstimator = TimeLeftEstimator()
+ notificationSwitcher.notify(startId, notification.create(job.progressValue, -1L))
+ job.progressAsFlow()
+ .onEach { state ->
+ if (state is DownloadState.Progress) {
+ timeLeftEstimator.tick(value = state.progress, total = state.max)
+ } else {
+ timeLeftEstimator.emptyTick()
+ }
}
+ .throttle { state -> if (state is DownloadState.Progress) 400L else 0L }
+ .whileActive()
+ .collect { state ->
+ val timeLeft = timeLeftEstimator.getEstimatedTimeLeft()
+ notificationSwitcher.notify(startId, notification.create(state, timeLeft))
+ }
+ job.join()
+ } finally {
+ (job.progressValue as? DownloadState.Done)?.let {
+ sendBroadcast(
+ Intent(ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false))
+ )
}
- .throttle { state -> if (state is DownloadState.Progress) 400L else 0L }
- .whileActive()
- .collect { state ->
- val timeLeft = timeLeftEstimator.getEstimatedTimeLeft()
- notificationSwitcher.notify(startId, notification.create(state, timeLeft))
- }
- job.join()
- (job.progressValue as? DownloadState.Done)?.let {
- sendBroadcast(
- Intent(ACTION_DOWNLOAD_COMPLETE)
- .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false))
+ notificationSwitcher.detach(
+ startId,
+ if (job.isCancelled) {
+ null
+ } else {
+ notification.create(job.progressValue, -1L)
+ }
)
+ stopSelf(startId)
}
- notificationSwitcher.detach(
- startId,
- if (job.isCancelled) {
- null
- } else {
- notification.create(job.progressValue, -1L)
- }
- )
- stopSelf(startId)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt
index 8222c0f2b..6d880abb9 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt
@@ -4,13 +4,14 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
+import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditViewModel
import org.koitharu.kotatsu.favourites.ui.categories.select.MangaCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel
val favouritesModule
get() = module {
- single { FavouritesRepository(get()) }
+ factory { FavouritesRepository(get(), get()) }
viewModel { categoryId ->
FavouritesListViewModel(categoryId.get(), get(), get(), get())
@@ -19,4 +20,5 @@ val favouritesModule
viewModel { manga ->
MangaCategoriesViewModel(manga.get(), get())
}
+ viewModel { params -> FavouritesCategoryEditViewModel(params[0], get(), get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt
index 801f2566a..c6a65c78e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt
@@ -11,4 +11,5 @@ fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong())
sortKey = sortKey,
order = SortOrder(order, SortOrder.NEWEST),
createdAt = Date(createdAt),
+ isTrackingEnabled = track,
)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt
index 436dc12ea..148dfd820 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt
@@ -6,6 +6,9 @@ import kotlinx.coroutines.flow.Flow
@Dao
abstract class FavouriteCategoriesDao {
+ @Query("SELECT * FROM favourite_categories WHERE category_id = :id")
+ abstract suspend fun find(id: Int): FavouriteCategoryEntity
+
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract suspend fun findAll(): List
@@ -13,7 +16,7 @@ abstract class FavouriteCategoriesDao {
abstract fun observeAll(): Flow>
@Query("SELECT * FROM favourite_categories WHERE category_id = :id")
- abstract fun observe(id: Long): Flow
+ abstract fun observe(id: Long): Flow
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
@@ -27,9 +30,15 @@ abstract class FavouriteCategoriesDao {
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun updateTitle(id: Long, title: String)
+ @Query("UPDATE favourite_categories SET title = :title, `order` = :order, `track` = :tracker WHERE category_id = :id")
+ abstract suspend fun update(id: Long, title: String, order: String, tracker: Boolean)
+
@Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id")
abstract suspend fun updateOrder(id: Long, order: String)
+ @Query("UPDATE favourite_categories SET `track` = :isEnabled WHERE category_id = :id")
+ abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
+
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
abstract suspend fun updateSortKey(id: Long, sortKey: Int)
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt
index 317a67162..08cbd7b62 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITE_CATEGORIES
+import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
@Entity(tableName = TABLE_FAVOURITE_CATEGORIES)
class FavouriteCategoryEntity(
@@ -13,4 +13,5 @@ class FavouriteCategoryEntity(
@ColumnInfo(name = "sort_key") val sortKey: Int,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "order") val order: String,
+ @ColumnInfo(name = "track") val track: Boolean,
)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt
index a423adc33..a13e54230 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt
@@ -3,11 +3,12 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITES
+import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity(
- tableName = TABLE_FAVOURITES, primaryKeys = ["manga_id", "category_id"],
+ tableName = TABLE_FAVOURITES,
+ primaryKeys = ["manga_id", "category_id"],
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
index 9e5da45f7..89fcc92fb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
@@ -43,6 +43,9 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List
+ @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE category_id = :categoryId)")
+ abstract suspend fun findAllManga(categoryId: Int): List
+
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
abstract suspend fun findAllManga(): List
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
index 731d7ff5e..13a4c92c4 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.favourites.domain
import androidx.room.withTransaction
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -13,9 +10,13 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
+import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
import org.koitharu.kotatsu.utils.ext.mapItems
-class FavouritesRepository(private val db: MangaDatabase) {
+class FavouritesRepository(
+ private val db: MangaDatabase,
+ private val channels: TrackerNotificationChannels,
+) {
suspend fun getAllManga(): List {
val entities = db.favouritesDao.findAll()
@@ -48,6 +49,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
}.distinctUntilChanged()
}
+ fun observeCategory(id: Long): Flow {
+ return db.favouriteCategoriesDao.observe(id)
+ .map { it?.toFavouriteCategory() }
+ }
+
fun observeCategories(mangaId: Long): Flow> {
return db.favouritesDao.observe(mangaId).map { entity ->
entity?.categories?.map { it.toFavouriteCategory() }.orEmpty()
@@ -58,6 +64,29 @@ class FavouritesRepository(private val db: MangaDatabase) {
return db.favouritesDao.observeIds(mangaId).map { it.toSet() }
}
+ suspend fun getCategory(id: Long): FavouriteCategory {
+ return db.favouriteCategoriesDao.find(id.toInt()).toFavouriteCategory()
+ }
+
+ suspend fun createCategory(title: String, sortOrder: SortOrder, isTrackerEnabled: Boolean): FavouriteCategory {
+ val entity = FavouriteCategoryEntity(
+ title = title,
+ createdAt = System.currentTimeMillis(),
+ sortKey = db.favouriteCategoriesDao.getNextSortKey(),
+ categoryId = 0,
+ order = sortOrder.name,
+ track = isTrackerEnabled,
+ )
+ val id = db.favouriteCategoriesDao.insert(entity)
+ val category = entity.toFavouriteCategory(id)
+ channels.createChannel(category)
+ return category
+ }
+
+ suspend fun updateCategory(id: Long, title: String, sortOrder: SortOrder, isTrackerEnabled: Boolean) {
+ db.favouriteCategoriesDao.update(id, title, sortOrder.name, isTrackerEnabled)
+ }
+
suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity(
title = title,
@@ -65,23 +94,32 @@ class FavouritesRepository(private val db: MangaDatabase) {
sortKey = db.favouriteCategoriesDao.getNextSortKey(),
categoryId = 0,
order = SortOrder.NEWEST.name,
+ track = true,
)
val id = db.favouriteCategoriesDao.insert(entity)
- return entity.toFavouriteCategory(id)
+ val category = entity.toFavouriteCategory(id)
+ channels.createChannel(category)
+ return category
}
suspend fun renameCategory(id: Long, title: String) {
db.favouriteCategoriesDao.updateTitle(id, title)
+ channels.renameChannel(id, title)
}
suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao.delete(id)
+ channels.deleteChannel(id)
}
suspend fun setCategoryOrder(id: Long, order: SortOrder) {
db.favouriteCategoriesDao.updateOrder(id, order.name)
}
+ suspend fun setCategoryTracking(id: Long, isEnabled: Boolean) {
+ db.favouriteCategoriesDao.updateTracking(id, isEnabled)
+ }
+
suspend fun reorderCategories(orderedIds: List) {
val dao = db.favouriteCategoriesDao
db.withTransaction {
@@ -121,6 +159,7 @@ class FavouritesRepository(private val db: MangaDatabase) {
private fun observeOrder(categoryId: Long): Flow {
return db.favouriteCategoriesDao.observe(categoryId)
+ .filterNotNull()
.map { x -> SortOrder(x.order, SortOrder.NEWEST) }
.distinctUntilChanged()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt
index 4433f978d..8e9d945a0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt
@@ -6,6 +6,7 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.children
+import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar
@@ -16,12 +17,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
-import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
+import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
+import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight
@@ -31,13 +33,15 @@ class FavouritesContainerFragment :
BaseFragment(),
FavouritesTabLongClickListener,
CategoriesEditDelegate.CategoriesEditCallback,
- ActionModeListener {
+ ActionModeListener,
+ View.OnClickListener {
private val viewModel by viewModel()
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this)
}
private var pagerAdapter: FavouritesPagerAdapter? = null
+ private var stubBinding: ItemEmptyStateBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -52,9 +56,7 @@ class FavouritesContainerFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = FavouritesPagerAdapter(this, this)
- viewModel.visibleCategories.value?.let {
- adapter.replaceData(it)
- }
+ viewModel.visibleCategories.value?.let(::onCategoriesChanged)
binding.pager.adapter = adapter
pagerAdapter = adapter
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
@@ -66,6 +68,7 @@ class FavouritesContainerFragment :
override fun onDestroyView() {
pagerAdapter = null
+ stubBinding = null
super.onDestroyView()
}
@@ -101,6 +104,15 @@ class FavouritesContainerFragment :
private fun onCategoriesChanged(categories: List) {
pagerAdapter?.replaceData(categories)
+ if (categories.isEmpty()) {
+ binding.pager.isVisible = false
+ binding.tabs.isVisible = false
+ showStub()
+ } else {
+ binding.pager.isVisible = true
+ binding.tabs.isVisible = true
+ (stubBinding?.root ?: binding.stubEmptyState).isVisible = false
+ }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -130,28 +142,16 @@ class FavouritesContainerFragment :
return true
}
+ override fun onClick(v: View) {
+ when (v.id) {
+ R.id.button_retry -> startActivity(FavouritesCategoryEditActivity.newIntent(v.context))
+ }
+ }
+
override fun onDeleteCategory(category: FavouriteCategory) {
viewModel.deleteCategory(category.id)
}
- override fun onRenameCategory(category: FavouriteCategory, newName: String) {
- viewModel.renameCategory(category.id, newName)
- }
-
- override fun onCreateCategory(name: String) {
- viewModel.createCategory(name)
- }
-
- private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
- val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
- for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
- val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes)
- menuItem.isCheckable = true
- menuItem.isChecked = item == category.order
- }
- submenu.setGroupCheckable(R.id.group_order, true, true)
- }
-
private fun TabLayout.setTabsEnabled(enabled: Boolean) {
val tabStrip = getChildAt(0) as? ViewGroup ?: return
for (tab in tabStrip.children) {
@@ -162,18 +162,11 @@ class FavouritesContainerFragment :
private fun showCategoryMenu(tabView: View, category: FavouriteCategory) {
val menu = PopupMenu(tabView.context, tabView)
menu.inflate(R.menu.popup_category)
- createOrderSubmenu(menu.menu, category)
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(category)
- R.id.action_rename -> editDelegate.renameCategory(category)
- R.id.action_create -> editDelegate.createCategory()
- R.id.action_order -> return@setOnMenuItemClickListener false
- else -> {
- val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order)
- ?: return@setOnMenuItemClickListener false
- viewModel.setCategoryOrder(category.id, order)
- }
+ R.id.action_edit -> startActivity(FavouritesCategoryEditActivity.newIntent(tabView.context, category.id))
+ else -> return@setOnMenuItemClickListener false
}
true
}
@@ -185,7 +178,7 @@ class FavouritesContainerFragment :
menu.inflate(R.menu.popup_category_all)
menu.setOnMenuItemClickListener {
when (it.itemId) {
- R.id.action_create -> editDelegate.createCategory()
+ R.id.action_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext()))
R.id.action_hide -> viewModel.setAllCategoriesVisible(false)
}
true
@@ -193,6 +186,18 @@ class FavouritesContainerFragment :
menu.show()
}
+ private fun showStub() {
+ val stub = stubBinding ?: ItemEmptyStateBinding.bind(binding.stubEmptyState.inflate())
+ stub.root.isVisible = true
+ stub.icon.setImageResource(R.drawable.ic_heart_outline)
+ stub.textPrimary.setText(R.string.text_empty_holder_primary)
+ stub.textSecondary.setText(R.string.empty_favourite_categories)
+ stub.buttonRetry.setText(R.string.add)
+ stub.buttonRetry.isVisible = true
+ stub.buttonRetry.setOnClickListener(this)
+ stubBinding = stub
+ }
+
companion object {
fun newInstance() = FavouritesContainerFragment()
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt
index 5a2eaf8df..80fd2a137 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.view.Menu
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
@@ -19,9 +18,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
-import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
+import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight
@@ -30,7 +29,8 @@ class CategoriesActivity :
BaseActivity(),
OnListItemClickListener,
View.OnClickListener,
- CategoriesEditDelegate.CategoriesEditCallback, AllCategoriesToggleListener {
+ CategoriesEditDelegate.CategoriesEditCallback,
+ AllCategoriesToggleListener {
private val viewModel by viewModel()
@@ -56,23 +56,17 @@ class CategoriesActivity :
override fun onClick(v: View) {
when (v.id) {
- R.id.fab_add -> editDelegate.createCategory()
+ R.id.fab_add -> startActivity(FavouritesCategoryEditActivity.newIntent(this))
}
}
override fun onItemClick(item: FavouriteCategory, view: View) {
val menu = PopupMenu(view.context, view)
menu.inflate(R.menu.popup_category)
- createOrderSubmenu(menu.menu, item)
menu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(item)
- R.id.action_rename -> editDelegate.renameCategory(item)
- R.id.action_order -> return@setOnMenuItemClickListener false
- else -> {
- val order = SORT_ORDERS.getOrNull(menuItem.order) ?: return@setOnMenuItemClickListener false
- viewModel.setCategoryOrder(item.id, order)
- }
+ R.id.action_edit -> startActivity(FavouritesCategoryEditActivity.newIntent(this, item.id))
}
true
}
@@ -116,29 +110,6 @@ class CategoriesActivity :
viewModel.deleteCategory(category.id)
}
- override fun onRenameCategory(category: FavouriteCategory, newName: String) {
- viewModel.renameCategory(category.id, newName)
- }
-
- override fun onCreateCategory(name: String) {
- viewModel.createCategory(name)
- }
-
- private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
- val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
- for ((i, item) in SORT_ORDERS.withIndex()) {
- val menuItem = submenu.add(
- R.id.group_order,
- Menu.NONE,
- i,
- item.titleRes
- )
- menuItem.isCheckable = true
- menuItem.isChecked = item == category.order
- }
- submenu.setGroupCheckable(R.id.group_order, true, true)
- }
-
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0
) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt
index e13b31e00..7a5620158 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt
@@ -40,7 +40,10 @@ class CategoriesAdapter(
newItem: CategoryListModel,
): Any? = when {
oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> Unit
- else -> super.getChangePayload(oldItem, newItem)
+ oldItem is CategoryListModel.CategoryItem &&
+ newItem is CategoryListModel.CategoryItem &&
+ oldItem.category.title != newItem.category.title -> null
+ else -> Unit
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt
index 603a70ee3..f7a98c078 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt
@@ -1,15 +1,10 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
-import android.text.InputType
-import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.FavouriteCategory
-private const val MAX_TITLE_LENGTH = 24
-
class CategoriesEditDelegate(
private val context: Context,
private val callback: CategoriesEditCallback
@@ -26,49 +21,8 @@ class CategoriesEditDelegate(
.show()
}
- fun renameCategory(category: FavouriteCategory) {
- TextInputDialog.Builder(context)
- .setTitle(R.string.rename)
- .setText(category.title)
- .setHint(R.string.enter_category_name)
- .setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
- .setNegativeButton(android.R.string.cancel)
- .setMaxLength(MAX_TITLE_LENGTH, false)
- .setPositiveButton(R.string.rename) { _, name ->
- val trimmed = name.trim()
- if (trimmed.isEmpty()) {
- Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show()
- } else {
- callback.onRenameCategory(category, name)
- }
- }.create()
- .show()
- }
-
- fun createCategory() {
- TextInputDialog.Builder(context)
- .setTitle(R.string.add_new_category)
- .setHint(R.string.enter_category_name)
- .setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
- .setNegativeButton(android.R.string.cancel)
- .setMaxLength(MAX_TITLE_LENGTH, false)
- .setPositiveButton(R.string.add) { _, name ->
- val trimmed = name.trim()
- if (trimmed.isEmpty()) {
- Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show()
- } else {
- callback.onCreateCategory(trimmed)
- }
- }.create()
- .show()
- }
-
interface CategoriesEditCallback {
fun onDeleteCategory(category: FavouriteCategory)
-
- fun onRenameCategory(category: FavouriteCategory, newName: String)
-
- fun onCreateCategory(name: String)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
index 7aac74e62..1e24d033f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
@@ -3,13 +3,13 @@ package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
-import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.*
@@ -31,33 +31,15 @@ class FavouritesCategoriesViewModel(
repository.observeCategories(),
observeAllCategoriesVisible(),
) { list, showAll ->
- mapCategories(list, showAll, showAll)
+ mapCategories(list, showAll, showAll && list.isNotEmpty())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
- fun createCategory(name: String) {
- launchJob {
- repository.addCategory(name)
- }
- }
-
- fun renameCategory(id: Long, name: String) {
- launchJob {
- repository.renameCategory(id, name)
- }
- }
-
fun deleteCategory(id: Long) {
launchJob {
repository.removeCategory(id)
}
}
- fun setCategoryOrder(id: Long, order: SortOrder) {
- launchJob {
- repository.setCategoryOrder(id, order)
- }
- }
-
fun setAllCategoriesVisible(isVisible: Boolean) {
settings.isAllFavouritesVisible = isVisible
}
@@ -89,9 +71,7 @@ class FavouritesCategoriesViewModel(
return result
}
- private fun observeAllCategoriesVisible() = settings.observe()
- .filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE }
- .map { settings.isAllFavouritesVisible }
- .onStart { emit(settings.isAllFavouritesVisible) }
- .distinctUntilChanged()
+ private fun observeAllCategoriesVisible() = settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) {
+ isAllFavouritesVisible
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt
index d840b783f..e64e36e5a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt
@@ -16,7 +16,7 @@ fun categoryAD(
clickListener.onItemClick(item.category, it)
}
@Suppress("ClickableViewAccessibility")
- binding.imageViewHandle.setOnTouchListener { v, event ->
+ binding.imageViewHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item.category, itemView)
} else {
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt
index 8326f8617..899b73e1c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt
@@ -45,6 +45,7 @@ sealed interface CategoryListModel : ListModel {
if (category.id != other.category.id) return false
if (category.title != other.category.title) return false
if (category.order != other.category.order) return false
+ if (category.isTrackingEnabled != other.category.isTrackingEnabled) return false
return true
}
@@ -53,6 +54,7 @@ sealed interface CategoryListModel : ListModel {
var result = category.id.hashCode()
result = 31 * result + category.title.hashCode()
result = 31 * result + category.order.hashCode()
+ result = 31 * result + category.isTrackingEnabled.hashCode()
return result
}
}
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
new file mode 100644
index 000000000..27239c0dc
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt
@@ -0,0 +1,147 @@
+package org.koitharu.kotatsu.favourites.ui.categories.edit
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import androidx.core.graphics.Insets
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.BaseActivity
+import org.koitharu.kotatsu.core.model.FavouriteCategory
+import org.koitharu.kotatsu.core.ui.titleRes
+import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding
+import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
+import org.koitharu.kotatsu.parsers.model.SortOrder
+import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+
+class FavouritesCategoryEditActivity : BaseActivity(), AdapterView.OnItemClickListener {
+
+ private val viewModel by viewModel {
+ parametersOf(intent.getLongExtra(EXTRA_ID, NO_ID))
+ }
+ private var selectedSortOrder: SortOrder? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityCategoryEditBinding.inflate(layoutInflater))
+ supportActionBar?.run {
+ setDisplayHomeAsUpEnabled(true)
+ setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material)
+ }
+ initSortSpinner()
+
+ viewModel.onSaved.observe(this) { finishAfterTransition() }
+ viewModel.category.observe(this, ::onCategoryChanged)
+ viewModel.isLoading.observe(this, ::onLoadingStateChanged)
+ viewModel.onError.observe(this, ::onError)
+ viewModel.isTrackerEnabled.observe(this) {
+ binding.switchTracker.isVisible = it
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putSerializable(KEY_SORT_ORDER, selectedSortOrder)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ val order = savedInstanceState.getSerializable(KEY_SORT_ORDER)
+ if (order != null && order is SortOrder) {
+ selectedSortOrder = order
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.opt_config, menu)
+ menu.findItem(R.id.action_done)?.setTitle(R.string.save)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.action_done -> {
+ viewModel.save(
+ title = binding.editName.text?.toString().orEmpty(),
+ sortOrder = getSelectedSortOrder(),
+ isTrackerEnabled = binding.switchTracker.isChecked,
+ )
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ binding.scrollView.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ bottom = insets.bottom,
+ )
+ binding.toolbar.updatePadding(
+ top = insets.top,
+ )
+ }
+
+ override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ selectedSortOrder = CategoriesActivity.SORT_ORDERS.getOrNull(position)
+ }
+
+ private fun onCategoryChanged(category: FavouriteCategory?) {
+ setTitle(if (category == null) R.string.create_category else R.string.edit_category)
+ if (selectedSortOrder != null) {
+ return
+ }
+ binding.editName.setText(category?.title)
+ selectedSortOrder = category?.order
+ val sortText = getString((category?.order ?: SortOrder.NEWEST).titleRes)
+ binding.editSort.setText(sortText, false)
+ binding.switchTracker.isChecked = category?.isTrackingEnabled ?: true
+ }
+
+ private fun onError(e: Throwable) {
+ binding.textViewError.text = e.getDisplayMessage(resources)
+ binding.textViewError.isVisible = true
+ }
+
+ private fun onLoadingStateChanged(isLoading: Boolean) {
+ binding.editSort.isEnabled = !isLoading
+ binding.editName.isEnabled = !isLoading
+ binding.switchTracker.isEnabled = !isLoading
+ if (isLoading) {
+ binding.textViewError.isVisible = false
+ }
+ }
+
+ private fun initSortSpinner() {
+ val entries = CategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) }
+ val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, entries)
+ binding.editSort.setAdapter(adapter)
+ binding.editSort.onItemClickListener = this
+ }
+
+ private fun getSelectedSortOrder(): SortOrder {
+ selectedSortOrder?.let { return it }
+ val entries = CategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) }
+ val index = entries.indexOf(binding.editSort.text.toString())
+ return CategoriesActivity.SORT_ORDERS.getOrNull(index) ?: SortOrder.NEWEST
+ }
+
+ companion object {
+
+ private const val EXTRA_ID = "id"
+ private const val KEY_SORT_ORDER = "sort"
+ private const val NO_ID = -1L
+
+ fun newIntent(context: Context, id: Long = NO_ID): Intent {
+ return Intent(context, FavouritesCategoryEditActivity::class.java)
+ .putExtra(EXTRA_ID, id)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt
new file mode 100644
index 000000000..78446dd14
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt
@@ -0,0 +1,53 @@
+package org.koitharu.kotatsu.favourites.ui.categories.edit
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.liveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.core.model.FavouriteCategory
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
+import org.koitharu.kotatsu.parsers.model.SortOrder
+import org.koitharu.kotatsu.utils.SingleLiveEvent
+
+private const val NO_ID = -1L
+
+class FavouritesCategoryEditViewModel(
+ private val categoryId: Long,
+ private val repository: FavouritesRepository,
+ private val settings: AppSettings,
+) : BaseViewModel() {
+
+ val onSaved = SingleLiveEvent()
+ val category = MutableLiveData()
+
+ val isTrackerEnabled = liveData(viewModelScope.coroutineContext + Dispatchers.Default) {
+ emit(settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources)
+ }
+
+ init {
+ launchLoadingJob {
+ category.value = if (categoryId != NO_ID) {
+ repository.getCategory(categoryId)
+ } else {
+ null
+ }
+ }
+ }
+
+ fun save(
+ title: String,
+ sortOrder: SortOrder,
+ isTrackerEnabled: Boolean,
+ ) {
+ launchLoadingJob {
+ if (categoryId == NO_ID) {
+ repository.createCategory(title, sortOrder, isTrackerEnabled)
+ } else {
+ repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled)
+ }
+ onSaved.call(Unit)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt
similarity index 86%
rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt
rename to app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt
index 43eec185e..aa2bacbbe 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt
@@ -17,26 +17,24 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.databinding.DialogFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
+import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
-class FavouriteCategoriesDialog :
+class FavouriteCategoriesBottomSheet :
BaseBottomSheet(),
OnListItemClickListener,
CategoriesEditDelegate.CategoriesEditCallback,
- Toolbar.OnMenuItemClickListener {
+ Toolbar.OnMenuItemClickListener, View.OnClickListener {
private val viewModel by viewModel {
parametersOf(requireNotNull(arguments?.getParcelableArrayList(KEY_MANGA_LIST)).map { it.manga })
}
private var adapter: MangaCategoriesAdapter? = null
- private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
- CategoriesEditDelegate(requireContext(), this@FavouriteCategoriesDialog)
- }
override fun onInflateView(
inflater: LayoutInflater,
@@ -48,6 +46,7 @@ class FavouriteCategoriesDialog :
adapter = MangaCategoriesAdapter(this)
binding.recyclerViewCategories.adapter = adapter
binding.toolbar.setOnMenuItemClickListener(this)
+ binding.itemCreate.setOnClickListener(this)
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
@@ -60,26 +59,26 @@ class FavouriteCategoriesDialog :
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
- R.id.action_create -> {
- editDelegate.createCategory()
+ R.id.action_done -> {
+ dismiss()
true
}
else -> false
}
}
+ override fun onClick(v: View) {
+ when (v.id) {
+ R.id.item_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext()))
+ }
+ }
+
override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.id, !item.isChecked)
}
override fun onDeleteCategory(category: FavouriteCategory) = Unit
- override fun onRenameCategory(category: FavouriteCategory, newName: String) = Unit
-
- override fun onCreateCategory(name: String) {
- viewModel.createCategory(name)
- }
-
private fun onContentChanged(categories: List) {
adapter?.items = categories
}
@@ -95,7 +94,7 @@ class FavouriteCategoriesDialog :
fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga))
- fun show(fm: FragmentManager, manga: Collection) = FavouriteCategoriesDialog().withArgs(1) {
+ fun show(fm: FragmentManager, manga: Collection) = FavouriteCategoriesBottomSheet().withArgs(1) {
putParcelableArrayList(
KEY_MANGA_LIST,
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withChapters = false) }
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt
index 84f9cf8f9..b9f906549 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt
@@ -38,12 +38,6 @@ class MangaCategoriesViewModel(
}
}
- fun createCategory(name: String) {
- launchJob(Dispatchers.Default) {
- favouritesRepository.addCategory(name)
- }
- }
-
private fun observeCategoriesIds() = if (manga.size == 1) {
// Fast path
favouritesRepository.observeCategoriesIds(manga[0].id)
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCaegoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt
similarity index 100%
rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCaegoryAD.kt
rename to app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt
index f2748622c..8d4b9e419 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt
@@ -1,11 +1,17 @@
package org.koitharu.kotatsu.favourites.ui.list
+import android.os.Bundle
import android.view.Menu
+import android.view.MenuInflater
import android.view.MenuItem
+import android.view.View
import androidx.appcompat.view.ActionMode
+import androidx.core.view.iterator
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.ui.titleRes
+import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -17,12 +23,54 @@ class FavouritesListFragment : MangaListFragment() {
}
private val categoryId: Long
- get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
+ get() = arguments?.getLong(ARG_CATEGORY_ID) ?: NO_ID
override val isSwipeRefreshEnabled = false
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
+ }
+
override fun onScrolledToEnd() = Unit
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+ if (categoryId != NO_ID) {
+ inflater.inflate(R.menu.opt_favourites_list, menu)
+ menu.findItem(R.id.action_order)?.subMenu?.let { submenu ->
+ for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
+ val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes)
+ menuItem.isCheckable = true
+ }
+ submenu.setGroupCheckable(R.id.group_order, true, true)
+ }
+ }
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ super.onPrepareOptionsMenu(menu)
+ menu.findItem(R.id.action_order)?.subMenu?.let { submenu ->
+ val selectedOrder = viewModel.sortOrder.value
+ for (item in submenu) {
+ val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order)
+ item.isChecked = order == selectedOrder
+ }
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when {
+ item.itemId == R.id.action_order -> false
+ item.groupId == R.id.group_order -> {
+ val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) ?: return false
+ viewModel.setSortOrder(order)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_favourites, menu)
return super.onCreateActionMode(mode, menu)
@@ -48,6 +96,7 @@ class FavouritesListFragment : MangaListFragment() {
companion object {
+ const val NO_ID = 0L
private const val ARG_CATEGORY_ID = "category_id"
fun newInstance(categoryId: Long) = FavouritesListFragment().withArgs(1) {
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 476fa6fb1..c2e8c00a1 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
@@ -1,12 +1,16 @@
package org.koitharu.kotatsu.favourites.ui.list
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
+import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.list.domain.CountersProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -24,8 +28,16 @@ class FavouritesListViewModel(
settings: AppSettings,
) : MangaListViewModel(settings), CountersProvider {
+ var sortOrder: LiveData = if (categoryId == NO_ID) {
+ MutableLiveData(null)
+ } else {
+ repository.observeCategory(categoryId)
+ .map { it?.order }
+ .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
+ }
+
override val content = combine(
- if (categoryId == 0L) {
+ if (categoryId == NO_ID) {
repository.observeAll(SortOrder.NEWEST)
} else {
repository.observeAll(categoryId)
@@ -37,7 +49,7 @@ class FavouritesListViewModel(
EmptyState(
icon = R.drawable.ic_heart_outline,
textPrimary = R.string.text_empty_holder_primary,
- textSecondary = if (categoryId == 0L) {
+ textSecondary = if (categoryId == NO_ID) {
R.string.you_have_not_favourites_yet
} else {
R.string.favourites_category_empty
@@ -60,7 +72,7 @@ class FavouritesListViewModel(
return
}
launchJob {
- if (categoryId == 0L) {
+ if (categoryId == NO_ID) {
repository.removeFromFavourites(ids)
} else {
repository.removeFromCategory(categoryId, ids)
@@ -68,6 +80,15 @@ class FavouritesListViewModel(
}
}
+ fun setSortOrder(order: SortOrder) {
+ if (categoryId == NO_ID) {
+ return
+ }
+ launchJob {
+ repository.setCategoryOrder(categoryId, order)
+ }
+ }
+
override suspend fun getCounter(mangaId: Long): Int {
return trackingRepository.getNewChaptersCount(mangaId)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt
index 74fb0ef40..246cb3a5f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt
@@ -8,6 +8,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel
val historyModule
get() = module {
- single { HistoryRepository(get(), get(), get()) }
+ factory { HistoryRepository(get(), get(), get()) }
viewModel { HistoryListViewModel(get(), get(), get(), get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
index fd221d330..bfd1f9598 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
@@ -12,6 +12,10 @@ abstract class HistoryDao {
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List
+ @Transaction
+ @Query("SELECT * FROM history WHERE manga_id IN (:ids)")
+ abstract suspend fun findAll(ids: Collection): List
+
@Transaction
@Query("SELECT * FROM history ORDER BY updated_at DESC")
abstract fun observeAll(): Flow>
@@ -66,4 +70,13 @@ abstract class HistoryDao {
true
} else false
}
+
+ @Transaction
+ open suspend fun upsert(entities: Iterable) {
+ for (e in entities) {
+ if (update(e) == 0) {
+ insert(e)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt
index 36b12937c..95edf3d9c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt
@@ -4,7 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_HISTORY
+import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity(
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
index a88c8a82d..a4b2ab772 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
@@ -4,6 +4,7 @@ import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
+import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.model.MangaHistory
@@ -100,6 +101,19 @@ class HistoryRepository(
}
}
+ suspend fun deleteReversible(ids: Collection): ReversibleHandle {
+ val entities = db.withTransaction {
+ val entities = db.historyDao.findAll(ids.toList()).filterNotNull()
+ for (id in ids) {
+ db.historyDao.delete(id)
+ }
+ entities
+ }
+ return ReversibleHandle {
+ db.historyDao.upsert(entities)
+ }
+ }
+
/**
* Try to replace one manga with another one
* Useful for replacing saved manga on deleting it with remove source
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
index 6980b80ee..27f4a86ca 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
@@ -7,8 +7,11 @@ import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.domain.ReversibleHandle
+import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -22,6 +25,7 @@ class HistoryListFragment : MangaListFragment() {
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu()
}
+ viewModel.onItemsRemoved.observe(viewLifecycleOwner, ::onItemsRemoved)
}
override fun onScrolledToEnd() = Unit
@@ -80,6 +84,12 @@ class HistoryListFragment : MangaListFragment() {
}
}
+ private fun onItemsRemoved(reversibleHandle: ReversibleHandle) {
+ Snackbar.make(binding.recyclerView, R.string.removed_from_history, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo) { reversibleHandle.reverseAsync() }
+ .show()
+ }
+
companion object {
fun newInstance() = HistoryListFragment()
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 42dd81e95..1768c2a5f 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
@@ -5,17 +5,24 @@ import androidx.lifecycle.viewModelScope
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.domain.ReversibleHandle
+import org.koitharu.kotatsu.base.domain.plus
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
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.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst
@@ -28,12 +35,9 @@ class HistoryListViewModel(
) : MangaListViewModel(settings) {
val isGroupingEnabled = MutableLiveData()
+ val onItemsRemoved = SingleLiveEvent()
- private val historyGrouping = settings.observe()
- .filter { it == AppSettings.KEY_HISTORY_GROUPING }
- .map { settings.historyGrouping }
- .onStart { emit(settings.historyGrouping) }
- .distinctUntilChanged()
+ private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping }
.onEach { isGroupingEnabled.postValue(it) }
override val content = combine(
@@ -52,8 +56,10 @@ class HistoryListViewModel(
)
else -> mapList(list, grouped, mode)
}
+ }.onStart {
+ loadingCounter.increment()
}.onFirst {
- isLoading.postValue(false)
+ loadingCounter.decrement()
}.catch {
it.toErrorState(canRetry = false)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
@@ -73,9 +79,12 @@ class HistoryListViewModel(
if (ids.isEmpty()) {
return
}
- launchJob {
- repository.delete(ids)
+ launchJob(Dispatchers.Default) {
+ val handle = repository.deleteReversible(ids) + ReversibleHandle {
+ shortcutsRepository.updateShortcuts()
+ }
shortcutsRepository.updateShortcuts()
+ onItemsRemoved.postCall(handle)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt b/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt
index 8e674701a..28f58887b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt
@@ -6,7 +6,6 @@ import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.view.ViewGroup
-import androidx.appcompat.app.AppCompatDelegate
import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.updateLayoutParams
@@ -14,7 +13,7 @@ import androidx.core.view.updatePadding
import coil.ImageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
-import coil.target.PoolableViewTarget
+import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koin.android.ext.android.inject
@@ -61,16 +60,12 @@ class ImageActivity : BaseActivity() {
private class SsivTarget(
override val view: SubsamplingScaleImageView,
- ) : PoolableViewTarget {
-
- override fun onStart(placeholder: Drawable?) = setDrawable(placeholder)
+ ) : ViewTarget {
override fun onError(error: Drawable?) = setDrawable(error)
override fun onSuccess(result: Drawable) = setDrawable(result)
- override fun onClear() = setDrawable(null)
-
override fun equals(other: Any?): Boolean {
return (this === other) || (other is SsivTarget && view == other.view)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
index 4b8c2b570..ad7e3ab70 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
@@ -28,7 +28,7 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.service.DownloadService
-import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
+import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -297,7 +297,7 @@ abstract class MangaListFragment :
true
}
R.id.action_favourite -> {
- FavouriteCategoriesDialog.show(childFragmentManager, selectedItems)
+ FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems)
mode.finish()
true
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
index 20f768c2f..6adc8c0d2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
@@ -4,16 +4,14 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaGridModel
-import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
-import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
-import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
abstract class MangaListViewModel(
private val settings: AppSettings,
@@ -21,20 +19,15 @@ abstract class MangaListViewModel(
abstract val content: LiveData>
val listMode = MutableLiveData()
- val gridScale = settings.observe()
- .filter { it == AppSettings.KEY_GRID_SIZE }
- .map { settings.gridSize / 100f }
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) {
- settings.gridSize / 100f
- }
+ val gridScale = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_GRID_SIZE,
+ valueProducer = { gridSize / 100f },
+ )
open fun onRemoveFilterTag(tag: MangaTag) = Unit
- protected fun createListModeFlow() = settings.observe()
- .filter { it == AppSettings.KEY_LIST_MODE }
- .map { settings.listMode }
- .onStart { emit(settings.listMode) }
- .distinctUntilChanged()
+ protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.onEach {
if (listMode.value != it) {
listMode.postValue(it)
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt
index 86b72c738..c13fd3cfa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt
@@ -13,7 +13,7 @@ fun currentFilterAD(
val chipGroup = itemView as ChipsView
- chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data ->
+ chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { _, data ->
listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
index ced1697b0..f1d6d3af4 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
+import coil.size.Scale
import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -43,6 +44,7 @@ fun mangaGridItemAD(
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
+ .scale(Scale.FILL)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
badge = itemView.bindBadge(badge, item.counter)
@@ -53,7 +55,7 @@ fun mangaGridItemAD(
badge = null
imageRequest?.dispose()
imageRequest = null
- CoilUtils.clear(binding.imageViewCover)
+ CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt
index 10e9a473d..74a67378f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
+import coil.size.Scale
import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -44,6 +45,7 @@ fun mangaListDetailedItemAD(
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
+ .scale(Scale.FILL)
.allowRgb565(true)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
@@ -57,7 +59,7 @@ fun mangaListDetailedItemAD(
badge = null
imageRequest?.dispose()
imageRequest = null
- CoilUtils.clear(binding.imageViewCover)
+ CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt
index 18696de6b..5087baa3f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
+import coil.size.Scale
import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -44,6 +45,7 @@ fun mangaListItemAD(
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
+ .scale(Scale.FILL)
.allowRgb565(true)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
@@ -55,7 +57,7 @@ fun mangaListItemAD(
badge = null
imageRequest?.dispose()
imageRequest = null
- CoilUtils.clear(binding.imageViewCover)
+ CoilUtils.dispose(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
}
}
\ No newline at end of file
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 81c79e1ae..7583b2e8c 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,7 +6,6 @@ import android.os.Bundle
import android.view.*
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.FragmentManager
-import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
@@ -14,11 +13,14 @@ import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import org.koitharu.kotatsu.utils.BottomSheetToolbarController
-class FilterBottomSheet : BaseBottomSheet(), MenuItem.OnActionExpandListener,
- SearchView.OnQueryTextListener, DialogInterface.OnKeyListener {
+class FilterBottomSheet :
+ BaseBottomSheet(),
+ MenuItem.OnActionExpandListener,
+ SearchView.OnQueryTextListener,
+ DialogInterface.OnKeyListener {
private val viewModel by sharedViewModel(
- owner = { from(requireParentFragment(), requireParentFragment()) }
+ owner = { requireParentFragment() }
)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt
index acba2466c..0cbb4fad7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt
@@ -1,18 +1,18 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.annotation.WorkerThread
+import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
-import java.util.*
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class FilterCoordinator(
private val repository: RemoteMangaRepository,
@@ -113,7 +113,7 @@ class FilterCoordinator(
FilterItem.Sort(it, isSelected = it == state.sortOrder)
}
}
- if(allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
+ if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
list.add(FilterItem.Header(R.string.genres, state.tags.size))
tags.mapTo(list) {
FilterItem.Tag(it, isChecked = it in state.tags)
@@ -153,9 +153,7 @@ class FilterCoordinator(
runCatching {
repository.getTags()
}.onFailure { error ->
- if (BuildConfig.DEBUG) {
- error.printStackTrace()
- }
+ error.printStackTraceDebug()
}.getOrNull()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt
index 3366248a4..f8cb28657 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.local.ui.LocalListViewModel
val localModule
get() = module {
- single { LocalStorageManager(androidContext(), get()) }
+ factory { LocalStorageManager(androidContext(), get()) }
single { LocalMangaRepository(get()) }
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
index ff04a2eff..c773a05d3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
@@ -2,41 +2,52 @@ package org.koitharu.kotatsu.local.data
import android.net.Uri
import android.webkit.MimeTypeMap
-import coil.bitmap.BitmapPool
+import coil.ImageLoader
import coil.decode.DataSource
-import coil.decode.Options
-import coil.fetch.FetchResult
+import coil.decode.ImageSource
import coil.fetch.Fetcher
import coil.fetch.SourceResult
-import coil.size.Size
-import java.util.zip.ZipFile
+import coil.request.Options
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.buffer
import okio.source
+import java.util.zip.ZipFile
-class CbzFetcher : Fetcher {
+class CbzFetcher(
+ private val uri: Uri,
+ private val options: Options
+) : Fetcher {
- override suspend fun fetch(
- pool: BitmapPool,
- data: Uri,
- size: Size,
- options: Options,
- ): FetchResult = runInterruptible(Dispatchers.IO) {
- val zip = ZipFile(data.schemeSpecificPart)
- val entry = zip.getEntry(data.fragment)
+ override suspend fun fetch() = runInterruptible(Dispatchers.IO) {
+ val zip = ZipFile(uri.schemeSpecificPart)
+ val entry = zip.getEntry(uri.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
+ val bufferedSource = ExtraCloseableBufferedSource(
+ zip.getInputStream(entry).source().buffer(),
+ zip,
+ )
SourceResult(
- source = ExtraCloseableBufferedSource(
- zip.getInputStream(entry).source().buffer(),
- zip,
+ source = ImageSource(
+ source = bufferedSource,
+ context = options.context,
+ metadata = CbzMetadata(uri),
),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext),
- dataSource = DataSource.DISK
+ dataSource = DataSource.DISK,
)
}
- override fun key(data: Uri) = data.toString()
+ class Factory : Fetcher.Factory {
- override fun handles(data: Uri) = data.scheme == "cbz"
+ override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
+ return if (data.scheme == "cbz") {
+ CbzFetcher(data, options)
+ } else {
+ null
+ }
+ }
+ }
+
+ class CbzMetadata(val uri: Uri) : ImageSource.Metadata()
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
index 4e5115ac8..fc2dbad03 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
@@ -15,11 +15,11 @@ import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.utils.ShareHelper
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.progress.Progress
class LocalListFragment : MangaListFragment(), ActivityResultCallback> {
@@ -68,9 +68,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback
- if (BuildConfig.DEBUG) {
- error.printStackTrace()
- }
+ error.printStackTraceDebug()
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt
index e8c69d824..c6e11107b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel
val mainModule
get() = module {
single { AppProtectHelper(get()) }
- single { ShortcutsRepository(androidContext(), get(), get(), get()) }
+ factory { ShortcutsRepository(androidContext(), get(), get(), get()) }
viewModel { MainViewModel(get(), get()) }
viewModel { ProtectViewModel(get(), get()) }
}
\ No newline at end of file
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 24805aa84..49bde074a 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
@@ -7,8 +7,10 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
+import androidx.activity.result.ActivityResultCallback
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.view.ActionMode
+import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.*
@@ -17,12 +19,13 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
+import androidx.transition.TransitionManager
import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -55,6 +58,7 @@ import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker
+import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.R as materialR
@@ -75,6 +79,7 @@ class MainActivity :
private lateinit var navHeaderBinding: NavigationHeaderBinding
private var drawerToggle: ActionBarDrawerToggle? = null
private var drawer: DrawerLayout? = null
+ private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
override val appBar: AppBarLayout
get() = binding.appbar
@@ -119,6 +124,7 @@ class MainActivity :
}
binding.fab.setOnClickListener(this@MainActivity)
+ binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let {
if (it is HistoryListFragment) binding.fab.show() else binding.fab.hide()
@@ -135,6 +141,7 @@ class MainActivity :
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.remoteSources.observe(this, this::updateSideMenu)
viewModel.isSuggestionsEnabled.observe(this, this::setSuggestionsEnabled)
+ viewModel.isTrackerEnabled.observe(this, this::setTrackerEnabled)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -277,6 +284,19 @@ class MainActivity :
searchSuggestionViewModel.onQueryChanged(query)
}
+ override fun onVoiceSearchClick() {
+ val options = binding.searchView.drawableEnd?.bounds?.let { bounds ->
+ ActivityOptionsCompat.makeScaleUpAnimation(
+ binding.searchView,
+ bounds.centerX(),
+ bounds.centerY(),
+ bounds.width(),
+ bounds.height(),
+ )
+ }
+ voiceInputLauncher.tryLaunch(binding.searchView.hint?.toString(), options)
+ }
+
override fun onClearSearchHistory() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.clear_search_history)
@@ -340,6 +360,14 @@ class MainActivity :
item.isVisible = isEnabled
}
+ private fun setTrackerEnabled(isEnabled: Boolean) {
+ val item = binding.navigationView.menu.findItem(R.id.nav_feed) ?: return
+ if (!isEnabled && item.isChecked) {
+ binding.navigationView.setCheckedItem(R.id.nav_history)
+ }
+ item.isVisible = isEnabled
+ }
+
private fun openDefaultSection() {
when (viewModel.defaultSection) {
AppSection.LOCAL -> {
@@ -373,32 +401,44 @@ class MainActivity :
}
private fun onSearchOpened() {
+ TransitionManager.beginDelayedTransition(binding.appbar)
drawerToggle?.isDrawerIndicatorEnabled = false
+ binding.toolbarCard.updateLayoutParams {
+ scrollFlags = SCROLL_FLAG_NO_SCROLL
+ }
+ binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
+ binding.appbar.updatePadding(left = 0, right = 0)
adjustDrawerLock()
adjustFabVisibility(isSearchOpened = true)
}
private fun onSearchClosed() {
+ TransitionManager.beginDelayedTransition(binding.appbar)
drawerToggle?.isDrawerIndicatorEnabled = true
+ binding.toolbarCard.updateLayoutParams {
+ scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS
+ }
+ binding.appbar.background = null
+ val padding = resources.getDimensionPixelOffset(R.dimen.margin_normal)
+ binding.appbar.updatePadding(left = padding, right = padding)
adjustDrawerLock()
adjustFabVisibility(isSearchOpened = false)
}
private fun onFirstStart() {
- lifecycleScope.launch(Dispatchers.Default) {
- TrackWorker.setup(applicationContext)
- SuggestionsWorker.setup(applicationContext)
- if (AppUpdateChecker.isUpdateSupported(this@MainActivity)) {
+ lifecycleScope.launchWhenResumed {
+ val isUpdateSupported = withContext(Dispatchers.Default) {
+ TrackWorker.setup(applicationContext)
+ SuggestionsWorker.setup(applicationContext)
+ AppUpdateChecker.isUpdateSupported(this@MainActivity)
+ }
+ if (isUpdateSupported) {
AppUpdateChecker(this@MainActivity).checkIfNeeded()
}
val settings = get()
when {
- !settings.isSourcesSelected -> withContext(Dispatchers.Main) {
- OnboardDialogFragment.showWelcome(supportFragmentManager)
- }
- settings.newSources.isNotEmpty() -> withContext(Dispatchers.Main) {
- NewSourcesDialogFragment.show(supportFragmentManager)
- }
+ !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
+ settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
}
}
@@ -427,4 +467,13 @@ class MainActivity :
if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED
)
}
+
+ private inner class VoiceInputCallback : ActivityResultCallback {
+
+ override fun onActivityResult(result: String?) {
+ if (result != null) {
+ binding.searchView.query = result
+ }
+ }
+ }
}
\ No newline at end of file
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 f2b98d7e0..c3a681343 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
@@ -7,7 +7,9 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
+import org.koitharu.kotatsu.core.prefs.AppSection
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent
@@ -15,17 +17,27 @@ import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class MainViewModel(
private val historyRepository: HistoryRepository,
- settings: AppSettings
+ private val settings: AppSettings,
) : BaseViewModel() {
val onOpenReader = SingleLiveEvent()
- var defaultSection by settings::defaultSection
+ var defaultSection: AppSection
+ get() = settings.defaultSection
+ set(value) {
+ settings.defaultSection = value
+ }
- val isSuggestionsEnabled = settings.observe()
- .filter { it == AppSettings.KEY_SUGGESTIONS }
- .onStart { emit("") }
- .map { settings.isSuggestionsEnabled }
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
+ val isSuggestionsEnabled = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_SUGGESTIONS,
+ valueProducer = { isSuggestionsEnabled },
+ )
+
+ val isTrackerEnabled = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_TRACKER_ENABLED,
+ valueProducer = { isTrackerEnabled },
+ )
val isResumeEnabled = historyRepository
.observeHasItems()
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt
index f27f061c6..a27fb9e8d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
val readerModule
get() = module {
- single { MangaDataRepository(get()) }
+ factory { MangaDataRepository(get()) }
single { PagesCache(get()) }
factory { PageSaveHelper(get(), androidContext()) }
@@ -26,6 +26,7 @@ val readerModule
shortcutsRepository = get(),
settings = get(),
pageSaveHelper = get(),
+ bookmarksRepository = get(),
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt
new file mode 100644
index 000000000..289f44386
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt
@@ -0,0 +1,27 @@
+package org.koitharu.kotatsu.reader.data
+
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaChapter
+
+fun Manga.filterChapters(branch: String?): Manga {
+ if (chapters.isNullOrEmpty()) return this
+ return copy(chapters = chapters?.filter { it.branch == branch })
+}
+
+private fun Manga.copy(chapters: List?) = Manga(
+ id = id,
+ title = title,
+ altTitle = altTitle,
+ url = url,
+ publicUrl = publicUrl,
+ rating = rating,
+ isNsfw = isNsfw,
+ coverUrl = coverUrl,
+ tags = tags,
+ state = state,
+ author = author,
+ largeCoverUrl = largeCoverUrl,
+ description = description,
+ chapters = chapters,
+ source = source,
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt
index a2d8f7ac5..3e19c7036 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt
@@ -2,19 +2,26 @@ package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.net.Uri
+import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException
+import org.koitharu.kotatsu.base.domain.MangaUtils
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage
+import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader
+import java.io.File
import kotlin.coroutines.Continuation
-import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resume
+private const val MAX_FILENAME_LENGTH = 10
+private const val EXTENSION_FALLBACK = "png"
+
class PageSaveHelper(
private val cache: PagesCache,
context: Context,
@@ -28,22 +35,17 @@ class PageSaveHelper(
page: MangaPage,
saveLauncher: ActivityResultLauncher,
): Uri {
- var pageFile = cache[page.url]
- var fileName = pageFile?.name
- if (fileName == null) {
- fileName = pageLoader.getPageUrl(page).toHttpUrl().pathSegments.last()
- }
- val cc = coroutineContext
- val destination = suspendCancellableCoroutine { cont ->
- continuation = cont
- Dispatchers.Main.dispatch(cc) {
- saveLauncher.launch(fileName)
+ val pageUrl = pageLoader.getPageUrl(page)
+ val pageFile = pageLoader.loadPage(page, force = false)
+ val proposedName = getProposedFileName(pageUrl, pageFile)
+ val destination = withContext(Dispatchers.Main) {
+ suspendCancellableCoroutine { cont ->
+ continuation = cont
+ saveLauncher.launch(proposedName)
+ }.also {
+ continuation = null
}
}
- continuation = null
- if (pageFile == null) {
- pageFile = pageLoader.loadPage(page, force = false)
- }
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output ->
pageFile.inputStream().use { input ->
@@ -57,4 +59,19 @@ class PageSaveHelper(
fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
resume(uri)
} != null
+
+ private suspend fun getProposedFileName(url: String, file: File): String {
+ var name = url.toHttpUrl().pathSegments.last()
+ var extension = name.substringAfterLast('.', "")
+ name = name.substringBeforeLast('.')
+ if (extension.length !in 2..4) {
+ val mimeType = MangaUtils.getImageMimeType(file)
+ extension = if (mimeType != null) {
+ MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
+ } else {
+ EXTENSION_FALLBACK
+ }
+ }
+ return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
+ }
}
\ No newline at end of file
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 ca9228207..e4a29b948 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
@@ -6,11 +6,13 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.*
-import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.core.graphics.Insets
-import androidx.core.view.*
-import androidx.fragment.app.commit
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.Slide
import androidx.transition.TransitionManager
@@ -29,6 +31,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseFullscreenActivity
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.ReaderMode
@@ -36,11 +39,7 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
-import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
-import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
-import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
-import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity
@@ -50,6 +49,8 @@ import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
+import org.koitharu.kotatsu.utils.ext.postDelayed
+import java.util.concurrent.TimeUnit
class ReaderActivity :
BaseFullscreenActivity(),
@@ -74,13 +75,13 @@ class ReaderActivity :
private lateinit var controlDelegate: ReaderControlDelegate
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var gestureInsets: Insets = Insets.NONE
-
- private val reader
- get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReader<*>
+ private lateinit var readerManager: ReaderManager
+ private val hideUiRunnable = Runnable { setUiIsVisible(false) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityReaderBinding.inflate(layoutInflater))
+ readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
orientationHelper = ScreenOrientationHelper(this)
@@ -90,6 +91,7 @@ class ReaderActivity :
insetsDelegate.interceptingWindowInsetsListener = this
orientationHelper.observeAutoOrientation()
+ .flowWithLifecycle(lifecycle)
.onEach {
binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it
}.launchIn(lifecycleScope)
@@ -103,36 +105,29 @@ class ReaderActivity :
onLoadingStateChanged(viewModel.isLoading.value == true)
}
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
+ viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
+ viewModel.onShowToast.observe(this) { msgId ->
+ Snackbar.make(binding.container, msgId, Snackbar.LENGTH_SHORT)
+ .setAnchorView(binding.appbarBottom)
+ .show()
+ }
}
private fun onInitReader(mode: ReaderMode) {
- val currentReader = reader
- when (mode) {
- ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) {
- supportFragmentManager.commit {
- replace(R.id.container, WebtoonReaderFragment())
- }
- }
- ReaderMode.REVERSED -> if (currentReader !is ReversedReaderFragment) {
- supportFragmentManager.commit {
- replace(R.id.container, ReversedReaderFragment())
- }
- }
- ReaderMode.STANDARD -> if (currentReader !is PagerReaderFragment) {
- supportFragmentManager.commit {
- replace(R.id.container, PagerReaderFragment())
- }
- }
+ if (readerManager.currentMode != mode) {
+ readerManager.replace(mode)
}
- binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).setIcon(
- when (mode) {
- ReaderMode.WEBTOON -> R.drawable.ic_script
- ReaderMode.REVERSED -> R.drawable.ic_read_reversed
- ReaderMode.STANDARD -> R.drawable.ic_book_page
- }
- )
- binding.appbarTop.postDelayed(1000) {
- setUiIsVisible(false)
+ val iconRes = when (mode) {
+ ReaderMode.WEBTOON -> R.drawable.ic_script
+ ReaderMode.REVERSED -> R.drawable.ic_read_reversed
+ ReaderMode.STANDARD -> R.drawable.ic_book_page
+ }
+ binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).run {
+ setIcon(iconRes)
+ setVisible(true)
+ }
+ if (binding.appbarTop.isVisible) {
+ lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
}
}
@@ -144,18 +139,8 @@ class ReaderActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reader_mode -> {
- ReaderConfigDialog.show(
- supportFragmentManager,
- when (reader) {
- is PagerReaderFragment -> ReaderMode.STANDARD
- is WebtoonReaderFragment -> ReaderMode.WEBTOON
- is ReversedReaderFragment -> ReaderMode.REVERSED
- else -> {
- showWaitWhileLoading()
- return false
- }
- }
- )
+ val currentMode = readerManager.currentMode ?: return false
+ ReaderConfigDialog.show(supportFragmentManager, currentMode)
}
R.id.action_settings -> {
startActivity(SettingsActivity.newReaderSettingsIntent(this))
@@ -177,17 +162,24 @@ class ReaderActivity :
supportFragmentManager,
pages,
title?.toString().orEmpty(),
- reader?.getCurrentState()?.page ?: -1
+ readerManager.currentReader?.getCurrentState()?.page ?: -1,
)
} else {
- showWaitWhileLoading()
+ return false
}
}
R.id.action_save_page -> {
viewModel.getCurrentPage()?.also { page ->
- viewModel.saveCurrentState(reader?.getCurrentState())
+ viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.saveCurrentPage(page, savePageRequest)
- } ?: showWaitWhileLoading()
+ } ?: return false
+ }
+ R.id.action_bookmark -> {
+ if (viewModel.isBookmarkAdded.value == true) {
+ viewModel.removeBookmark()
+ } else {
+ viewModel.addBookmark()
+ }
}
else -> return super.onOptionsItemSelected(item)
}
@@ -202,10 +194,14 @@ class ReaderActivity :
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
binding.layoutLoading.isVisible = isLoading && !hasPages
if (isLoading && hasPages) {
- binding.toastView.show(R.string.loading_, true)
+ binding.toastView.show(R.string.loading_)
} else {
binding.toastView.hide()
}
+ val menu = binding.toolbarBottom.menu
+ menu.findItem(R.id.action_bookmark).isVisible = hasPages
+ menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages
+ menu.findItem(R.id.action_save_page).isVisible = hasPages
}
private fun onError(e: Throwable) {
@@ -265,14 +261,14 @@ class ReaderActivity :
val index = pages.indexOfFirst { it.id == page.id }
if (index != -1) {
withContext(Dispatchers.Main) {
- reader?.switchPageTo(index, true)
+ readerManager.currentReader?.switchPageTo(index, true)
}
}
}
}
override fun onReaderModeChanged(mode: ReaderMode) {
- viewModel.saveCurrentState(reader?.getCurrentState())
+ viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.switchMode(mode)
}
@@ -290,12 +286,6 @@ class ReaderActivity :
}
}
- private fun showWaitWhileLoading() {
- Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply {
- setGravity(Gravity.CENTER, 0, 0)
- }.show()
- }
-
private fun setWindowSecure(isSecure: Boolean) {
if (isSecure) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@@ -309,8 +299,8 @@ class ReaderActivity :
val transition = TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(Slide(Gravity.TOP).addTarget(binding.appbarTop))
- binding.appbarBottom?.let { botomBar ->
- transition.addTransition(Slide(Gravity.BOTTOM).addTarget(botomBar))
+ binding.appbarBottom?.let { bottomBar ->
+ transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar))
}
TransitionManager.beginDelayedTransition(binding.root, transition)
binding.appbarTop.isVisible = isUiVisible
@@ -344,13 +334,19 @@ class ReaderActivity :
override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun switchPageBy(delta: Int) {
- reader?.switchPageBy(delta)
+ readerManager.currentReader?.switchPageBy(delta)
}
override fun toggleUiVisibility() {
setUiIsVisible(!binding.appbarTop.isVisible)
}
+ private fun onBookmarkStateChanged(isAdded: Boolean) {
+ val menuItem = binding.toolbarBottom.menu.findItem(R.id.action_bookmark) ?: return
+ menuItem.setTitle(if (isAdded) R.string.bookmark_remove else R.string.bookmark_add)
+ menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
+ }
+
private fun onUiStateChanged(uiState: ReaderUiState, previous: ReaderUiState?) {
title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_)
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
@@ -419,6 +415,11 @@ class ReaderActivity :
.putExtra(EXTRA_STATE, state)
}
+ fun newIntent(context: Context, bookmark: Bookmark): Intent {
+ val state = ReaderState(bookmark.chapterId, bookmark.page, bookmark.scroll)
+ return newIntent(context, bookmark.manga, state)
+ }
+
fun newIntent(context: Context, mangaId: Long): Intent {
return Intent(context, ReaderActivity::class.java)
.putExtra(MangaIntent.KEY_ID, mangaId)
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 f8c5d73c0..dbe853894 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
@@ -5,14 +5,16 @@ import android.view.SoundEffectConstants
import android.view.View
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.utils.GridTouchHelper
-@Suppress("UNUSED_PARAMETER")
class ReaderControlDelegate(
- private val scope: LifecycleCoroutineScope,
- private val settings: AppSettings,
+ scope: LifecycleCoroutineScope,
+ settings: AppSettings,
private val listener: OnInteractionListener
) {
@@ -20,12 +22,8 @@ class ReaderControlDelegate(
private var isVolumeKeysSwitchEnabled: Boolean = false
init {
- settings.observe()
- .filter { it == AppSettings.KEY_READER_SWITCHERS }
- .map { settings.readerPageSwitch }
- .onStart { emit(settings.readerPageSwitch) }
- .distinctUntilChanged()
- .flowOn(Dispatchers.IO)
+ 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
@@ -57,7 +55,7 @@ class ReaderControlDelegate(
}
}
- fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) {
+ fun onKeyDown(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean = when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
listener.switchPageBy(-1)
true
@@ -92,9 +90,11 @@ class ReaderControlDelegate(
else -> false
}
- fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
- return (isVolumeKeysSwitchEnabled &&
- (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP))
+ fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean {
+ return (
+ isVolumeKeysSwitchEnabled &&
+ (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)
+ )
}
interface OnInteractionListener {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt
new file mode 100644
index 000000000..c5497fe8a
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt
@@ -0,0 +1,45 @@
+package org.koitharu.kotatsu.reader.ui
+
+import androidx.annotation.IdRes
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.commit
+import org.koitharu.kotatsu.core.prefs.ReaderMode
+import org.koitharu.kotatsu.reader.ui.pager.BaseReader
+import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
+import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
+import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
+import java.util.*
+
+class ReaderManager(
+ private val fragmentManager: FragmentManager,
+ @IdRes private val containerResId: Int,
+) {
+
+ private val modeMap = EnumMap>>(ReaderMode::class.java)
+
+ init {
+ modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java
+ modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java
+ modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
+ }
+
+ val currentReader: BaseReader<*>?
+ get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*>
+
+ val currentMode: ReaderMode?
+ get() {
+ val readerClass = currentReader?.javaClass ?: return null
+ return modeMap.entries.find { it.value == readerClass }?.key
+ }
+
+ fun replace(newMode: ReaderMode) {
+ val readerClass = requireNotNull(modeMap[newMode])
+ fragmentManager.commit {
+ replace(containerResId, readerClass, null, null)
+ }
+ }
+
+ fun replace(reader: BaseReader<*>) {
+ fragmentManager.commit { replace(containerResId, reader) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt
index 0fe72b499..2eb7cc960 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt
@@ -9,23 +9,20 @@ import org.koitharu.kotatsu.parsers.model.Manga
data class ReaderState(
val chapterId: Long,
val page: Int,
- val scroll: Int
+ val scroll: Int,
) : Parcelable {
- companion object {
+ constructor(history: MangaHistory) : this(
+ chapterId = history.chapterId,
+ page = history.page,
+ scroll = history.scroll,
+ )
- fun from(history: MangaHistory) = ReaderState(
- chapterId = history.chapterId,
- page = history.page,
- scroll = history.scroll
- )
-
- fun initial(manga: Manga, branch: String?) = ReaderState(
- chapterId = manga.chapters?.firstOrNull {
- it.branch == branch
- }?.id ?: error("Cannot find first chapter"),
- page = 0,
- scroll = 0
- )
- }
+ constructor(manga: Manga, branch: String?) : this(
+ chapterId = manga.chapters?.firstOrNull {
+ it.branch == branch
+ }?.id ?: error("Cannot find first chapter"),
+ page = 0,
+ scroll = 0,
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt
index f9852f4c6..a2acc8df7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt
@@ -1,13 +1,11 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
-import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.isVisible
-import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.transition.Fade
import androidx.transition.Slide
import androidx.transition.TransitionManager
@@ -15,26 +13,28 @@ import androidx.transition.TransitionSet
import com.google.android.material.textview.MaterialTextView
class ReaderToastView @JvmOverloads constructor(
- context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
) : MaterialTextView(context, attrs, defStyleAttr) {
private var hideRunnable = Runnable {
hide()
}
- fun show(message: CharSequence, isLoading: Boolean) {
+ fun show(message: CharSequence) {
removeCallbacks(hideRunnable)
text = message
setupTransition()
isVisible = true
}
- fun show(@StringRes messageId: Int, isLoading: Boolean) {
- show(context.getString(messageId), isLoading)
+ fun show(@StringRes messageId: Int) {
+ show(context.getString(messageId))
}
fun showTemporary(message: CharSequence, duration: Long) {
- show(message, false)
+ show(message)
postDelayed(hideRunnable, duration)
}
@@ -49,7 +49,7 @@ class ReaderToastView @JvmOverloads constructor(
super.onDetachedFromWindow()
}
- private fun setupTransition () {
+ private fun setupTransition() {
val parentView = parent as? ViewGroup ?: return
val transition = TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
@@ -58,14 +58,4 @@ class ReaderToastView @JvmOverloads constructor(
.addTransition(Fade())
TransitionManager.beginDelayedTransition(parentView, transition)
}
-
- // FIXME use it as compound drawable
- private fun createProgressDrawable(): CircularProgressDrawable {
- val drawable = CircularProgressDrawable(context)
- drawable.setStyle(CircularProgressDrawable.DEFAULT)
- drawable.arrowEnabled = false
- drawable.setColorSchemeColors(Color.WHITE)
- drawable.centerRadius = lineHeight / 3f
- return drawable
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index bfe5ac663..931461de0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -3,35 +3,40 @@ package org.koitharu.kotatsu.reader.ui
import android.net.Uri
import android.util.LongSparseArray
import androidx.activity.result.ActivityResultLauncher
+import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
+import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.get
-import org.koitharu.kotatsu.BuildConfig
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.domain.MangaUtils
import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.bookmarks.domain.Bookmark
+import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.core.prefs.ReaderMode
-import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
+import org.koitharu.kotatsu.core.prefs.*
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
+import org.koitharu.kotatsu.reader.data.filterChapters
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.SingleLiveEvent
-import org.koitharu.kotatsu.utils.ext.IgnoreErrors
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
+private const val BOUNDS_PAGE_OFFSET = 2
+private const val PAGES_TRIM_THRESHOLD = 120
+private const val PREFETCH_LIMIT = 10
+
class ReaderViewModel(
private val intent: MangaIntent,
initialState: ReaderState?,
@@ -39,12 +44,14 @@ class ReaderViewModel(
private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository,
+ private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings,
private val pageSaveHelper: PageSaveHelper,
) : BaseViewModel() {
private var loadingJob: Job? = null
private var pageSaveJob: Job? = null
+ private var bookmarkJob: Job? = null
private val currentState = MutableStateFlow(initialState)
private val mangaData = MutableStateFlow(intent.manga)
private val chapters = LongSparseArray()
@@ -53,6 +60,7 @@ class ReaderViewModel(
val readerMode = MutableLiveData()
val onPageSaved = SingleLiveEvent()
+ val onShowToast = SingleLiveEvent()
val uiState = combine(
mangaData,
currentState,
@@ -70,25 +78,32 @@ class ReaderViewModel(
val manga: Manga?
get() = mangaData.value
- val readerAnimation = settings.observe()
- .filter { it == AppSettings.KEY_READER_ANIMATION }
- .map { settings.readerAnimation }
- .onStart { emit(settings.readerAnimation) }
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO)
+ val readerAnimation = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_READER_ANIMATION,
+ valueProducer = { readerAnimation }
+ )
val isScreenshotsBlockEnabled = combine(
mangaData,
- settings.observe()
- .filter { it == AppSettings.KEY_SCREENSHOTS_POLICY }
- .onStart { emit("") }
- .map { settings.screenshotsPolicy },
+ settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
) { manga, policy ->
policy == ScreenshotsPolicy.BLOCK_ALL ||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
- }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO)
+ }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val onZoomChanged = SingleLiveEvent()
+ val isBookmarkAdded: LiveData = currentState.flatMapLatest { state ->
+ val manga = mangaData.value
+ if (state == null || manga == null) {
+ flowOf(false)
+ } else {
+ bookmarksRepository.observeBookmark(manga, state.chapterId, state.page)
+ .map { it != null }
+ }
+ }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
+
init {
loadImpl()
subscribeToSettings()
@@ -124,7 +139,7 @@ class ReaderViewModel(
if (state != null) {
currentState.value = state
}
- saveState(
+ historyRepository.saveStateAsync(
mangaData.value ?: return,
state ?: currentState.value ?: return
)
@@ -151,9 +166,7 @@ class ReaderViewModel(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
onPageSaved.postCall(null)
}
}
@@ -187,10 +200,9 @@ class ReaderViewModel(
fun onCurrentPageChanged(position: Int) {
val pages = content.value?.pages ?: return
- pages.getOrNull(position)?.let {
- val currentValue = currentState.value
- if (currentValue != null && currentValue.chapterId != it.chapterId) {
- currentState.value = currentValue.copy(chapterId = it.chapterId)
+ pages.getOrNull(position)?.let { page ->
+ currentState.update { cs ->
+ cs?.copy(chapterId = page.chapterId, page = page.index)
}
}
if (pages.isEmpty() || loadingJob?.isActive == true) {
@@ -207,6 +219,41 @@ class ReaderViewModel(
}
}
+ fun addBookmark() {
+ if (bookmarkJob?.isActive == true) {
+ return
+ }
+ bookmarkJob = launchJob {
+ loadingJob?.join()
+ val state = checkNotNull(currentState.value)
+ val page = checkNotNull(getCurrentPage()) { "Page not found" }
+ val bookmark = Bookmark(
+ manga = checkNotNull(mangaData.value),
+ pageId = page.id,
+ chapterId = state.chapterId,
+ page = state.page,
+ scroll = state.scroll,
+ imageUrl = page.preview ?: pageLoader.getPageUrl(page),
+ createdAt = Date(),
+ )
+ bookmarksRepository.addBookmark(bookmark)
+ onShowToast.call(R.string.bookmark_added)
+ }
+ }
+
+ fun removeBookmark() {
+ if (bookmarkJob?.isActive == true) {
+ return
+ }
+ bookmarkJob = launchJob {
+ loadingJob?.join()
+ val manga = checkNotNull(mangaData.value)
+ val page = checkNotNull(getCurrentPage()) { "Page not found" }
+ bookmarksRepository.removeBookmark(manga.id, page.id)
+ onShowToast.call(R.string.bookmark_removed)
+ }
+ }
+
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga")
@@ -217,24 +264,16 @@ class ReaderViewModel(
chapters.put(it.id, it)
}
// determine mode
- val mode = dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
- val pages = repo.getPages(it)
- val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
- val newMode = getReaderMode(isWebtoon)
- if (isWebtoon != null) {
- dataRepository.savePreferences(manga, newMode)
- }
- newMode
- } ?: error("There are no chapters in this manga")
+ val mode = detectReaderMode(manga, repo)
// obtain state
if (currentState.value == null) {
currentState.value = historyRepository.getOne(manga)?.let {
- ReaderState.from(it)
- } ?: ReaderState.initial(manga, preselectedBranch)
+ ReaderState(it)
+ } ?: ReaderState(manga, preselectedBranch)
}
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
- mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch })
+ mangaData.value = manga.filterChapters(branch)
readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
@@ -248,18 +287,12 @@ class ReaderViewModel(
}
}
- private fun getReaderMode(isWebtoon: Boolean?) = when {
- isWebtoon == true -> ReaderMode.WEBTOON
- settings.isPreferRtlReader -> ReaderMode.REVERSED
- else -> ReaderMode.STANDARD
- }
-
private suspend fun loadChapter(chapterId: Long): List {
val manga = checkNotNull(mangaData.value) { "Manga is null" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
val repo = MangaRepository(manga.source)
return repo.getPages(chapter).mapIndexed { index, page ->
- ReaderPage.from(page, index, chapterId)
+ ReaderPage(page, index, chapterId)
}
}
@@ -297,9 +330,9 @@ class ReaderViewModel(
private fun subscribeToSettings() {
settings.observe()
- .filter { it == AppSettings.KEY_ZOOM_MODE }
- .onEach { onZoomChanged.postCall(Unit) }
- .launchIn(viewModelScope + Dispatchers.IO)
+ .onEach { key ->
+ if (key == AppSettings.KEY_ZOOM_MODE) onZoomChanged.postCall(Unit)
+ }.launchIn(viewModelScope + Dispatchers.Default)
}
private fun List.trySublist(fromIndex: Int, toIndex: Int): List {
@@ -312,39 +345,42 @@ class ReaderViewModel(
}
}
- private fun Manga.copy(chapters: List?) = Manga(
- id = id,
- title = title,
- altTitle = altTitle,
- url = url,
- publicUrl = publicUrl,
- rating = rating,
- isNsfw = isNsfw,
- coverUrl = coverUrl,
- tags = tags,
- state = state,
- author = author,
- largeCoverUrl = largeCoverUrl,
- description = description,
- chapters = chapters,
- source = source,
- )
+ private suspend fun detectReaderMode(manga: Manga, repo: MangaRepository): ReaderMode {
+ dataRepository.getReaderMode(manga.id)?.let { return it }
+ val defaultMode = settings.defaultReaderMode
+ if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
+ return defaultMode
+ }
+ val chapter = currentState.value?.chapterId?.let(chapters::get)
+ ?: manga.chapters?.randomOrNull()
+ ?: error("There are no chapters in this manga")
+ val pages = repo.getPages(chapter)
+ return runCatching {
+ val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
+ if (isWebtoon) ReaderMode.WEBTOON else defaultMode
+ }.onSuccess {
+ dataRepository.savePreferences(manga, it)
+ }.onFailure {
+ it.printStackTraceDebug()
+ }.getOrDefault(defaultMode)
+ }
+}
- private companion object : KoinComponent {
-
- const val BOUNDS_PAGE_OFFSET = 2
- const val PAGES_TRIM_THRESHOLD = 120
- const val PREFETCH_LIMIT = 10
-
- fun saveState(manga: Manga, state: ReaderState) {
- processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) {
- get().addOrUpdate(
- manga = manga,
- chapterId = state.chapterId,
- page = state.page,
- scroll = state.scroll
- )
- }
+/**
+ * This function is not a member of the ReaderViewModel
+ * because it should work independently of the ViewModel's lifecycle.
+ */
+private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState): Job {
+ return processLifecycleScope.launch(Dispatchers.Default) {
+ runCatching {
+ addOrUpdate(
+ manga = manga,
+ chapterId = state.chapterId,
+ page = state.page,
+ scroll = state.scroll
+ )
+ }.onFailure {
+ it.printStackTraceDebug()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt
index bc3f3220e..6773f5dc3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt
@@ -13,27 +13,24 @@ data class ReaderPage(
val preview: String?,
val chapterId: Long,
val index: Int,
- val source: MangaSource
+ val source: MangaSource,
) : Parcelable {
+ constructor(page: MangaPage, index: Int, chapterId: Long) : this(
+ id = page.id,
+ url = page.url,
+ referer = page.referer,
+ preview = page.preview,
+ chapterId = chapterId,
+ index = index,
+ source = page.source,
+ )
+
fun toMangaPage() = MangaPage(
id = id,
url = url,
referer = referer,
preview = preview,
- source = source
+ source = source,
)
-
- companion object {
-
- fun from(page: MangaPage, index: Int, chapterId: Long) = ReaderPage(
- id = page.id,
- url = page.url,
- referer = page.referer,
- preview = page.preview,
- chapterId = chapterId,
- index = index,
- source = page.source
- )
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt
index 201a4f4ae..8cddae963 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import android.graphics.drawable.Drawable
import coil.ImageLoader
import coil.request.ImageRequest
-import coil.size.PixelSize
+import coil.size.Size
import com.google.android.material.R as materialR
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.*
@@ -27,7 +27,7 @@ fun pageThumbnailAD(
var job: Job? = null
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
- val thumbSize = PixelSize(
+ val thumbSize = Size(
width = gridWidth,
height = (gridWidth * 13f / 18f).toInt()
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
index b2a540baa..b57f0b30b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
@@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.*
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
@@ -21,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
private const val FILTER_MIN_INTERVAL = 750L
@@ -133,12 +133,10 @@ class RemoteListViewModel(
}
hasNextPage.value = list.isNotEmpty()
} catch (e: Throwable) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
listError.value = e
if (!mangaList.value.isNullOrEmpty()) {
- onError.postCall(e)
+ errorEvent.postCall(e)
}
}
}
@@ -158,4 +156,4 @@ class RemoteListViewModel(
textSecondary = 0,
actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter,
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
index 1f14c6ee3..1d1fb43fc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt
@@ -13,8 +13,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
val searchModule
get() = module {
- single { MangaSearchRepository(get(), get(), androidContext(), get()) }
-
+ factory { MangaSearchRepository(get(), get(), androidContext(), get()) }
factory { MangaSuggestionsProvider.createSuggestions(androidContext()) }
viewModel { params ->
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt
index b516a026a..3511f3b3d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt
@@ -67,19 +67,19 @@ class GlobalSearchViewModel(
searchJob = repository.globalSearch(query)
.catch { e ->
listError.value = e
- isLoading.postValue(false)
+ loadingCounter.reset()
}.onStart {
mangaList.value = null
listError.value = null
- isLoading.postValue(true)
+ loadingCounter.increment()
hasNextPage.value = true
}.onEmpty {
mangaList.value = emptyList()
}.onCompletion {
- isLoading.postValue(false)
+ loadingCounter.reset()
hasNextPage.value = false
}.onFirst {
- isLoading.postValue(false)
+ loadingCounter.reset()
}.onEach {
mangaList.value = mangaList.value?.plus(it) ?: listOf(it)
}.launchIn(viewModelScope + Dispatchers.Default)
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt
index 9a1ffba8f..7d9a3b6cb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt
@@ -9,6 +9,7 @@ import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding
import org.koitharu.kotatsu.main.ui.AppBarOwner
@@ -43,11 +44,10 @@ class SearchSuggestionFragment :
override fun onWindowInsetsChanged(insets: Insets) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
+ val extraPadding = resources.getDimensionPixelOffset(R.dimen.list_spacing)
binding.root.updatePadding(
- top = headerHeight,
- // left = insets.left,
- // right = insets.right,
- bottom = insets.bottom,
+ top = headerHeight + extraPadding,
+ bottom = insets.bottom + extraPadding,
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt
index 9a942009b..ea9dfd6f2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt
@@ -14,4 +14,6 @@ interface SearchSuggestionListener {
fun onClearSearchHistory()
fun onTagClick(tag: MangaTag)
+
+ fun onVoiceSearchClick()
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt
index 07e24ca16..261648fce 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt
@@ -2,15 +2,20 @@ package org.koitharu.kotatsu.search.ui.widget
import android.annotation.SuppressLint
import android.content.Context
+import android.os.Parcelable
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.MotionEvent
+import android.view.SoundEffectConstants
+import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.EditorInfo
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.content.ContextCompat
-import com.google.android.material.R
+import com.google.android.material.R as materialR
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
+import org.koitharu.kotatsu.utils.ext.drawableEnd
import org.koitharu.kotatsu.utils.ext.drawableStart
private const val DRAWABLE_END = 2
@@ -18,11 +23,19 @@ private const val DRAWABLE_END = 2
class SearchEditText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
- @AttrRes defStyleAttr: Int = R.attr.editTextStyle,
+ @AttrRes defStyleAttr: Int = materialR.attr.editTextStyle,
) : AppCompatEditText(context, attrs, defStyleAttr) {
var searchSuggestionListener: SearchSuggestionListener? = null
- private val clearIcon = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_material)
+ private val clearIcon = ContextCompat.getDrawable(context, materialR.drawable.abc_ic_clear_material)
+ private val voiceIcon = ContextCompat.getDrawable(context, R.drawable.ic_voice_input)
+ private var isEmpty = text.isNullOrEmpty()
+
+ var isVoiceSearchEnabled: Boolean = false
+ set(value) {
+ field = value
+ updateActionIcon()
+ }
var query: String
get() = text?.trim()?.toString().orEmpty()
@@ -57,15 +70,19 @@ class SearchEditText @JvmOverloads constructor(
lengthAfter: Int,
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
- setCompoundDrawablesRelativeWithIntrinsicBounds(
- drawableStart,
- null,
- if (text.isNullOrEmpty()) null else clearIcon,
- null,
- )
+ val empty = text.isNullOrEmpty()
+ if (isEmpty != empty) {
+ isEmpty = empty
+ updateActionIcon()
+ }
searchSuggestionListener?.onQueryChanged(query)
}
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ super.onRestoreInstanceState(state)
+ updateActionIcon()
+ }
+
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP) {
@@ -76,7 +93,9 @@ class SearchEditText @JvmOverloads constructor(
event.x.toInt() in (width - drawable.bounds.width() - paddingRight)..(width - paddingRight)
}
if (isOnDrawable) {
- text?.clear()
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
+ playSoundEffect(SoundEffectConstants.CLICK)
+ onActionIconClick()
return true
}
}
@@ -87,4 +106,22 @@ class SearchEditText @JvmOverloads constructor(
super.clearFocus()
text?.clear()
}
+
+ private fun onActionIconClick() {
+ when {
+ !text.isNullOrEmpty() -> text?.clear()
+ isVoiceSearchEnabled -> searchSuggestionListener?.onVoiceSearchClick()
+ }
+ }
+
+ private fun updateActionIcon() {
+ val icon = when {
+ !text.isNullOrEmpty() -> clearIcon
+ isVoiceSearchEnabled -> voiceIcon
+ else -> null
+ }
+ if (icon !== drawableEnd) {
+ setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, icon, null)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
index 12d7f21ce..a9e2ab345 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
@@ -19,6 +19,7 @@ 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
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
@@ -45,8 +46,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
suspend fun checkNow() = runCatching {
val version = repo.getLatestVersion()
- val newVersionId = VersionId.parse(version.name)
- val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
+ val newVersionId = VersionId(version.name)
+ val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
val result = newVersionId > currentVersionId
if (result) {
withContext(Dispatchers.Main) {
@@ -56,30 +57,27 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
settings.lastUpdateCheckTimestamp = System.currentTimeMillis()
result
}.onFailure {
- it.printStackTrace()
+ 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)
.setTitle(R.string.app_update_available)
- .setMessage(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)
- })
+ .setMessage(message)
.setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
}
.setNegativeButton(R.string.close, null)
+ .setCancelable(false)
.create()
.show()
}
@@ -102,7 +100,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
return null
}
val signatures = packageInfo?.signatures
@@ -112,7 +110,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
return null
}
return try {
@@ -120,12 +118,12 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
null
} catch (e: CertificateEncodingException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
null
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
index 36031ec21..81817e8b5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
@@ -12,9 +12,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.settings.utils.SliderPreference
-import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import java.util.*
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt
index 8bfd5e39a..800599441 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt
@@ -3,18 +3,22 @@ package org.koitharu.kotatsu.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
+import androidx.preference.ListPreference
import androidx.preference.Preference
-import java.io.File
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
+import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.LocalStorageManager
+import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.utils.ext.getStorageName
+import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
+import java.io.File
class ContentSettingsFragment :
BasePreferenceFragment(R.string.content),
@@ -36,6 +40,15 @@ class ContentSettingsFragment :
true
}
}
+ findPreference(AppSettings.KEY_DOH)?.run {
+ entryValues = arrayOf(
+ DoHProvider.NONE,
+ DoHProvider.GOOGLE,
+ DoHProvider.CLOUDFLARE,
+ DoHProvider.ADGUARD,
+ ).names()
+ setDefaultValueCompat(DoHProvider.NONE.name)
+ }
bindRemoteSourcesSummary()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt
index b8852f47e..ee15c796a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings
import android.content.Context
+import android.content.SharedPreferences
import android.media.RingtoneManager
import android.os.Bundle
import android.view.View
@@ -11,7 +12,9 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.utils.RingtonePickContract
-class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notifications) {
+class NotificationSettingsLegacyFragment :
+ BasePreferenceFragment(R.string.notifications),
+ SharedPreferences.OnSharedPreferenceChangeListener {
private val ringtonePickContract = registerForActivityResult(
RingtonePickContract(get().getString(R.string.notification_sound))
@@ -25,15 +28,28 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_notifications)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
findPreference(AppSettings.KEY_NOTIFICATIONS_SOUND)?.run {
val uri = settings.notificationSound
summary = RingtoneManager.getRingtone(context, uri)?.getTitle(context)
?: getString(R.string.silent)
}
+ updateInfo()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ settings.subscribe(this)
+ }
+
+ override fun onDestroyView() {
+ settings.unsubscribe(this)
+ super.onDestroyView()
+ }
+
+ override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
+ when (key) {
+ AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateInfo()
+ }
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
@@ -45,4 +61,9 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif
else -> super.onPreferenceTreeClick(preference)
}
}
-}
+
+ private fun updateInfo() {
+ findPreference(AppSettings.KEY_NOTIFICATIONS_INFO)
+ ?.isVisible = !settings.isTrackerNotificationsEnabled
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt
index 5602d23c2..39f4de6c5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt
@@ -1,26 +1,68 @@
package org.koitharu.kotatsu.settings
+import android.content.SharedPreferences
import android.os.Bundle
+import android.view.View
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
+import androidx.preference.Preference
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.ReaderMode
+import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
-import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
-class ReaderSettingsFragment : BasePreferenceFragment(R.string.reader_settings) {
+class ReaderSettingsFragment :
+ BasePreferenceFragment(R.string.reader_settings),
+ SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_reader)
- findPreference(AppSettings.KEY_READER_SWITCHERS)?.let {
- it.summaryProvider = MultiSummaryProvider(R.string.gestures_only)
+ findPreference(AppSettings.KEY_READER_MODE)?.run {
+ entryValues = arrayOf(
+ ReaderMode.STANDARD,
+ ReaderMode.REVERSED,
+ ReaderMode.WEBTOON,
+ ).names()
+ setDefaultValueCompat(ReaderMode.STANDARD.name)
}
- findPreference(AppSettings.KEY_ZOOM_MODE)?.let {
- it.entryValues = ZoomMode.values().names()
- it.setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
+ findPreference(AppSettings.KEY_READER_SWITCHERS)?.run {
+ summaryProvider = MultiSummaryProvider(R.string.gestures_only)
+ }
+ findPreference(AppSettings.KEY_ZOOM_MODE)?.run {
+ entryValues = arrayOf(
+ ZoomMode.FIT_CENTER,
+ ZoomMode.FIT_HEIGHT,
+ ZoomMode.FIT_WIDTH,
+ ZoomMode.KEEP_START,
+ ).names()
+ setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
+ }
+ updateReaderModeDependency()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ settings.subscribe(this)
+ }
+
+ override fun onDestroyView() {
+ settings.unsubscribe(this)
+ super.onDestroyView()
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ when (key) {
+ AppSettings.KEY_READER_MODE -> updateReaderModeDependency()
+ }
+ }
+
+ private fun updateReaderModeDependency() {
+ findPreference(AppSettings.KEY_READER_MODE_DETECT)?.run {
+ isEnabled = settings.defaultReaderMode != ReaderMode.WEBTOON
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt
index b1fd14c50..230ab0d32 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt
@@ -17,8 +17,8 @@ import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
val settingsModule
get() = module {
- single { BackupRepository(get()) }
- single { RestoreRepository(get()) }
+ factory { BackupRepository(get()) }
+ factory { RestoreRepository(get()) }
single(createdAtStart = true) { AppSettings(androidContext()) }
viewModel { BackupViewModel(get(), androidContext()) }
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 384905df2..4ffa13c5b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
@@ -6,7 +6,6 @@ import androidx.preference.Preference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -14,6 +13,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -70,9 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
preference.title = getString(R.string.logged_in_as, username)
}.onFailure { error ->
preference.isEnabled = error is AuthRequiredException
- if (BuildConfig.DEBUG) {
- error.printStackTrace()
- }
+ error.printStackTraceDebug()
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt
index 1e993f845..f1637def6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt
@@ -1,21 +1,34 @@
package org.koitharu.kotatsu.settings
import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.text.style.URLSpan
+import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
-import org.koitharu.kotatsu.tracker.work.TrackWorker
+import org.koitharu.kotatsu.tracker.domain.TrackingRepository
+import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
+import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
-class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_chapters) {
+class TrackerSettingsFragment :
+ BasePreferenceFragment(R.string.check_for_new_chapters),
+ SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private val repository by inject()
+ private val channels by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_tracker)
@@ -32,22 +45,81 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch
}
}
}
+ updateCategoriesEnabled()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateCategoriesSummary()
+ updateNotificationsSummary()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ settings.subscribe(this)
+ }
+
+ override fun onDestroyView() {
+ settings.unsubscribe(this)
+ super.onDestroyView()
+ }
+
+ override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) {
+ when (key) {
+ AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateNotificationsSummary()
+ AppSettings.KEY_TRACK_SOURCES,
+ AppSettings.KEY_TRACKER_ENABLED -> updateCategoriesEnabled()
+ }
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
- AppSettings.KEY_NOTIFICATIONS_SETTINGS -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
+ AppSettings.KEY_NOTIFICATIONS_SETTINGS -> when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
+ val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
- .putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID)
startActivity(intent)
true
- } else {
+ }
+ channels.areNotificationsDisabled -> {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", requireContext().packageName, null))
+ startActivity(intent)
+ true
+ }
+ else -> {
super.onPreferenceTreeClick(preference)
}
}
+ AppSettings.KEY_TRACK_CATEGORIES -> {
+ startActivity(CategoriesActivity.newIntent(preference.context))
+ true
+ }
else -> super.onPreferenceTreeClick(preference)
}
}
+
+ private fun updateNotificationsSummary() {
+ val pref = findPreference(AppSettings.KEY_NOTIFICATIONS_SETTINGS) ?: return
+ pref.setSummary(
+ when {
+ channels.areNotificationsDisabled -> R.string.disabled
+ channels.isNotificationGroupEnabled() -> R.string.show_notification_new_chapters_on
+ else -> R.string.show_notification_new_chapters_off
+ }
+ )
+ }
+
+ private fun updateCategoriesEnabled() {
+ val pref = findPreference(AppSettings.KEY_TRACK_CATEGORIES) ?: return
+ pref.isEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources
+ }
+
+ private fun updateCategoriesSummary() {
+ val pref = findPreference(AppSettings.KEY_TRACK_CATEGORIES) ?: return
+ viewLifecycleScope.launch {
+ val count = repository.getCategoriesCount()
+ pref.summary = getString(R.string.enabled_d_of_d, count[0], count[1])
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt
index 278995dab..baa2a5217 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt
@@ -51,7 +51,7 @@ class AppBackupAgent : BackupAgent() {
}
private fun createBackupFile() = runBlocking {
- val repository = BackupRepository(MangaDatabase.create(applicationContext))
+ val repository = BackupRepository(MangaDatabase(applicationContext))
BackupZipOutput(this@AppBackupAgent).use { backup ->
backup.put(repository.createIndex())
backup.put(repository.dumpHistory())
@@ -63,7 +63,7 @@ class AppBackupAgent : BackupAgent() {
}
private fun restoreBackupFile(fd: FileDescriptor, size: Long) {
- val repository = RestoreRepository(MangaDatabase.create(applicationContext))
+ val repository = RestoreRepository(MangaDatabase(applicationContext))
val tempFile = File.createTempFile("backup_", ".tmp")
FileInputStream(fd).use { input ->
tempFile.outputStream().use { output ->
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
index b41ebb205..53ad51cbc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
@@ -7,12 +7,13 @@ import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference
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.prefs.AppSettings
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
-class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
+class BackupSettingsFragment :
+ BasePreferenceFragment(R.string.backup_restore),
ActivityResultCallback {
private val backupSelectCall = registerForActivityResult(
@@ -34,9 +35,7 @@ class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
try {
backupSelectCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
Snackbar.make(
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT
).show()
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 92fee1db9..4f695b154 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
@@ -18,8 +18,10 @@ import org.koitharu.kotatsu.utils.ext.observeNotNull
import org.koitharu.kotatsu.utils.ext.showAllowStateLoss
import org.koitharu.kotatsu.utils.ext.withArgs
-class OnboardDialogFragment : AlertDialogFragment(),
- OnListItemClickListener, DialogInterface.OnClickListener {
+class OnboardDialogFragment :
+ AlertDialogFragment(),
+ OnListItemClickListener,
+ DialogInterface.OnClickListener {
private val viewModel by viewModel()
private var isWelcome: Boolean = false
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt
index f2a929017..2f1495243 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.onboard
import androidx.collection.ArraySet
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.MutableLiveData
-import java.util.*
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -12,6 +11,7 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.mapToSet
+import java.util.*
class OnboardViewModel(
private val settings: AppSettings,
@@ -55,6 +55,7 @@ class OnboardViewModel(
settings.hiddenSources = allSources.filterNot { x ->
x.locale in selectedLocales
}.mapToSet { x -> x.name }
+ settings.markKnownSources(settings.newSources)
}
private fun rebuildList() {
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 634eeb757..832922ea3 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
@@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
@@ -37,8 +38,10 @@ class SuggestionsViewModel(
list.toUi(this, mode)
}
}
+ }.onStart {
+ loadingCounter.increment()
}.onFirst {
- isLoading.postValue(false)
+ loadingCounter.decrement()
}.catch {
it.toErrorState(canRetry = false)
}.asLiveDataDistinct(
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt
index 8bba4be33..115f41621 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt
@@ -10,12 +10,7 @@ import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITES
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITE_CATEGORIES
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_HISTORY
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA_TAGS
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_TAGS
+import org.koitharu.kotatsu.core.db.*
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.sync.data.AccountAuthenticator
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt
index 0c159a53b..02f296aac 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt
@@ -8,15 +8,9 @@ import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import androidx.sqlite.db.SupportSQLiteQueryBuilder
-import java.util.concurrent.Callable
import org.koin.android.ext.android.inject
-import org.koitharu.kotatsu.core.db.MangaDatabase
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITES
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITE_CATEGORIES
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_HISTORY
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA_TAGS
-import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_TAGS
+import org.koitharu.kotatsu.core.db.*
+import java.util.concurrent.Callable
abstract class SyncProvider : ContentProvider() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt
index a08495f54..975e96d66 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt
@@ -1,14 +1,17 @@
package org.koitharu.kotatsu.tracker
+import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.ui.FeedViewModel
+import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
val trackerModule
get() = module {
- single { TrackingRepository(get()) }
+ factory { TrackingRepository(get()) }
+ factory { TrackerNotificationChannels(androidContext(), get()) }
viewModel { FeedViewModel(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt
index 661f68bba..aefa9a69a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt
@@ -2,15 +2,15 @@ package org.koitharu.kotatsu.tracker.domain
import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase
-import org.koitharu.kotatsu.core.db.entity.TrackEntity
-import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
-import org.koitharu.kotatsu.core.db.entity.toManga
-import org.koitharu.kotatsu.core.db.entity.toTrackingLogItem
+import org.koitharu.kotatsu.core.db.entity.*
+import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaTracking
import org.koitharu.kotatsu.core.model.TrackingLogItem
+import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.*
class TrackingRepository(
@@ -21,16 +21,29 @@ class TrackingRepository(
return db.tracksDao.findNewChapters(mangaId) ?: 0
}
- suspend fun getAllTracks(useFavourites: Boolean, useHistory: Boolean): List {
- val mangaList = ArrayList()
- if (useFavourites) {
- db.favouritesDao.findAllManga().mapTo(mangaList) { it.toManga(emptySet()) }
+ suspend fun getHistoryManga(): List {
+ return db.historyDao.findAllManga().toMangaList()
+ }
+
+ suspend fun getFavouritesManga(): Map> {
+ val categories = db.favouriteCategoriesDao.findAll()
+ return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
+ categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList()
}
- if (useHistory) {
- db.historyDao.findAllManga().mapTo(mangaList) { it.toManga(emptySet()) }
- }
- val tracks = db.tracksDao.findAll().groupBy { it.mangaId }
- return mangaList
+ }
+
+ suspend fun getCategoriesCount(): IntArray {
+ val categories = db.favouriteCategoriesDao.findAll()
+ return intArrayOf(
+ categories.count { it.track },
+ categories.size,
+ )
+ }
+
+ suspend fun getTracks(mangaList: Collection): List {
+ val ids = mangaList.mapToSet { it.id }
+ val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
+ return mangaList // TODO optimize
.filterNot { it.source == MangaSource.LOCAL }
.distinctBy { it.id }
.map { manga ->
@@ -103,4 +116,6 @@ class TrackingRepository(
)
db.tracksDao.upsert(entity)
}
+
+ private fun Collection.toMangaList() = map { it.toManga(emptySet()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt
index 292d2c982..0170ee01d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt
@@ -65,6 +65,9 @@ class FeedViewModel(
if (loadingJob?.isActive == true) {
return
}
+ if (append && !hasNextPage.value) {
+ return
+ }
loadingJob = launchLoadingJob(Dispatchers.Default) {
val offset = if (append) logList.value?.size ?: 0 else 0
val list = repository.getTrackingLog(offset, 20)
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt
index 74e5468b9..000ff5399 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.tracker.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
+import coil.size.Scale
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -34,6 +35,7 @@ fun feedItemAD(
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.allowRgb565(true)
+ .scale(Scale.FILL)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
binding.textViewTitle.text = item.title
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt
index a61b25c47..f1e40a04b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt
@@ -5,14 +5,16 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.os.Build
-import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC
+import androidx.core.app.NotificationCompat.VISIBILITY_SECRET
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.work.*
import coil.ImageLoader
import coil.request.ImageRequest
+import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
@@ -29,7 +31,6 @@ import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground
import org.koitharu.kotatsu.utils.progress.Progress
-import java.util.concurrent.TimeUnit
class TrackWorker(context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams), KoinComponent {
@@ -41,26 +42,22 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
private val coil by inject()
private val repository by inject()
private val settings by inject()
+ private val channels by inject()
override suspend fun doWork(): Result {
- val trackSources = settings.trackSources
- if (trackSources.isEmpty()) {
- return Result.success()
- }
- val tracks = repository.getAllTracks(
- useFavourites = AppSettings.TRACK_FAVOURITES in trackSources,
- useHistory = AppSettings.TRACK_HISTORY in trackSources
- )
- if (tracks.isEmpty()) {
+ if (!settings.isTrackerEnabled) {
return Result.success()
}
if (TAG in tags) { // not expedited
trySetForeground()
}
+ val tracks = getAllTracks()
+
var success = 0
val workData = Data.Builder()
.putInt(DATA_TOTAL, tracks.size)
- for ((index, track) in tracks.withIndex()) {
+ for ((index, item) in tracks.withIndex()) {
+ val (track, channelId) = item
val details = runCatching {
MangaRepository(track.manga.source).getDetails(track.manga)
}.getOrNull()
@@ -80,12 +77,12 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { // manga was empty on last check
repository.storeTrackResult(
mangaId = track.manga.id,
- knownChaptersCount = track.knownChaptersCount,
+ knownChaptersCount = 0,
lastChapterId = 0L,
previousTrackChapterId = track.lastNotifiedChapterId,
newChapters = chapters
)
- showNotification(details, chapters)
+ showNotification(details, channelId, chapters)
}
chapters.size == track.knownChaptersCount -> {
if (chapters.lastOrNull()?.id == track.lastChapterId) {
@@ -114,7 +111,8 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
)
showNotification(
details,
- newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId }
+ channelId,
+ newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId },
)
}
}
@@ -126,11 +124,12 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
knownChaptersCount = track.knownChaptersCount,
lastChapterId = track.lastChapterId,
previousTrackChapterId = track.lastNotifiedChapterId,
- newChapters = newChapters
+ newChapters = newChapters,
)
showNotification(
- track.manga,
- newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId }
+ manga = track.manga,
+ channelId = channelId,
+ newChapters = newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId },
)
}
}
@@ -144,13 +143,60 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
}
}
- private suspend fun showNotification(manga: Manga, newChapters: List) {
- if (newChapters.isEmpty() || !settings.trackerNotifications) {
+ private suspend fun getAllTracks(): List {
+ val sources = settings.trackSources
+ if (sources.isEmpty()) {
+ return emptyList()
+ }
+ val knownIds = HashSet()
+ val result = ArrayList()
+ // Favourites
+ if (AppSettings.TRACK_FAVOURITES in sources) {
+ val favourites = repository.getFavouritesManga()
+ channels.updateChannels(favourites.keys)
+ for ((category, mangaList) in favourites) {
+ if (!category.isTrackingEnabled || mangaList.isEmpty()) {
+ continue
+ }
+ val categoryTracks = repository.getTracks(mangaList)
+ val channelId = if (channels.isFavouriteNotificationsEnabled(category)) {
+ channels.getFavouritesChannelId(category.id)
+ } else {
+ null
+ }
+ for (track in categoryTracks) {
+ if (knownIds.add(track.manga)) {
+ result.add(TrackingItem(track, channelId))
+ }
+ }
+ }
+ }
+ // History
+ if (AppSettings.TRACK_HISTORY in sources) {
+ val history = repository.getHistoryManga()
+ val historyTracks = repository.getTracks(history)
+ val channelId = if (channels.isHistoryNotificationsEnabled()) {
+ channels.getHistoryChannelId()
+ } else {
+ null
+ }
+ for (track in historyTracks) {
+ if (knownIds.add(track.manga)) {
+ result.add(TrackingItem(track, channelId))
+ }
+ }
+ }
+ result.trimToSize()
+ return result
+ }
+
+ private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List) {
+ if (newChapters.isEmpty() || channelId == null) {
return
}
val id = manga.url.hashCode()
val colorPrimary = ContextCompat.getColor(applicationContext, R.color.blue_primary)
- val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
+ val builder = NotificationCompat.Builder(applicationContext, channelId)
val summary = applicationContext.resources.getQuantityString(
R.plurals.new_chapters,
newChapters.size, newChapters.size
@@ -183,6 +229,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
)
)
setAutoCancel(true)
+ setVisibility(if (manga.isNsfw) VISIBILITY_SECRET else VISIBILITY_PUBLIC)
color = colorPrimary
setShortcutId(manga.id.toString())
priority = NotificationCompat.PRIORITY_DEFAULT
@@ -236,7 +283,6 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
companion object {
- const val CHANNEL_ID = "tracking"
private const val WORKER_CHANNEL_ID = "track_worker"
private const val WORKER_NOTIFICATION_ID = 35
private const val DATA_PROGRESS = "progress"
@@ -244,27 +290,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
private const val TAG = "tracking"
private const val TAG_ONESHOT = "tracking_oneshot"
- @RequiresApi(Build.VERSION_CODES.O)
- private fun createNotificationChannel(context: Context) {
- val manager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- if (manager.getNotificationChannel(CHANNEL_ID) == null) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- context.getString(R.string.new_chapters),
- NotificationManager.IMPORTANCE_DEFAULT
- )
- channel.setShowBadge(true)
- channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary_dark)
- channel.enableLights(true)
- manager.createNotificationChannel(channel)
- }
- }
-
fun setup(context: Context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- createNotificationChannel(context)
- }
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt
new file mode 100644
index 000000000..81fcb73d5
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt
@@ -0,0 +1,143 @@
+package org.koitharu.kotatsu.tracker.work
+
+import android.app.NotificationChannel
+import android.app.NotificationChannelGroup
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationManagerCompat
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.FavouriteCategory
+import org.koitharu.kotatsu.core.prefs.AppSettings
+
+class TrackerNotificationChannels(
+ private val context: Context,
+ private val settings: AppSettings,
+) {
+
+ private val manager = NotificationManagerCompat.from(context)
+
+ val areNotificationsDisabled: Boolean
+ get() = !manager.areNotificationsEnabled()
+
+ fun updateChannels(categories: Collection) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return
+ }
+ manager.deleteNotificationChannel(OLD_CHANNEL_ID)
+ val group = createGroup()
+ val existingChannels = group.channels.associateByTo(HashMap()) { it.id }
+ for (category in categories) {
+ val id = getFavouritesChannelId(category.id)
+ if (existingChannels.remove(id)?.name == category.title) {
+ continue
+ }
+ val channel = NotificationChannel(id, category.title, NotificationManager.IMPORTANCE_DEFAULT)
+ channel.group = GROUP_ID
+ manager.createNotificationChannel(channel)
+ }
+ existingChannels.remove(CHANNEL_ID_HISTORY)
+ createHistoryChannel()
+ for (id in existingChannels.keys) {
+ manager.deleteNotificationChannel(id)
+ }
+ }
+
+ fun createChannel(category: FavouriteCategory) {
+ renameChannel(category.id, category.title)
+ }
+
+ fun renameChannel(categoryId: Long, name: String) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return
+ }
+ val id = getFavouritesChannelId(categoryId)
+ val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
+ channel.group = createGroup().id
+ manager.createNotificationChannel(channel)
+ }
+
+ fun deleteChannel(categoryId: Long) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return
+ }
+ manager.deleteNotificationChannel(getFavouritesChannelId(categoryId))
+ }
+
+ fun isFavouriteNotificationsEnabled(category: FavouriteCategory): Boolean {
+ if (!manager.areNotificationsEnabled()) {
+ return false
+ }
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = manager.getNotificationChannel(getFavouritesChannelId(category.id))
+ channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE
+ } else {
+ // fallback
+ settings.isTrackerNotificationsEnabled
+ }
+ }
+
+ fun isHistoryNotificationsEnabled(): Boolean {
+ if (!manager.areNotificationsEnabled()) {
+ return false
+ }
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = manager.getNotificationChannel(getHistoryChannelId())
+ channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE
+ } else {
+ // fallback
+ settings.isTrackerNotificationsEnabled
+ }
+ }
+
+ fun isNotificationGroupEnabled(): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return settings.isTrackerNotificationsEnabled
+ }
+ val group = manager.getNotificationChannelGroup(GROUP_ID) ?: return true
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && group.isBlocked) {
+ return false
+ }
+ return group.channels.any { it.importance != NotificationManagerCompat.IMPORTANCE_NONE }
+ }
+
+ fun getFavouritesChannelId(categoryId: Long): String {
+ return CHANNEL_ID_PREFIX + categoryId
+ }
+
+ fun getHistoryChannelId(): String {
+ return CHANNEL_ID_HISTORY
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createGroup(): NotificationChannelGroup {
+ manager.getNotificationChannelGroup(GROUP_ID)?.let {
+ return it
+ }
+ val group = NotificationChannelGroup(GROUP_ID, context.getString(R.string.new_chapters))
+ manager.createNotificationChannelGroup(group)
+ return group
+ }
+
+ private fun createHistoryChannel() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return
+ }
+ val channel = NotificationChannel(
+ CHANNEL_ID_HISTORY,
+ context.getString(R.string.history),
+ NotificationManager.IMPORTANCE_DEFAULT,
+ )
+ channel.group = GROUP_ID
+ manager.createNotificationChannel(channel)
+ }
+
+ companion object {
+
+ const val GROUP_ID = "trackers"
+ private const val CHANNEL_ID_PREFIX = "track_fav_"
+ private const val CHANNEL_ID_HISTORY = "track_history"
+ private const val OLD_CHANNEL_ID = "tracking"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt
new file mode 100644
index 000000000..933918009
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt
@@ -0,0 +1,31 @@
+package org.koitharu.kotatsu.tracker.work
+
+import org.koitharu.kotatsu.core.model.MangaTracking
+
+class TrackingItem(
+ val tracking: MangaTracking,
+ val channelId: String?,
+) {
+
+ operator fun component1() = tracking
+
+ operator fun component2() = channelId
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as TrackingItem
+
+ if (tracking != other.tracking) return false
+ if (channelId != other.channelId) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = tracking.hashCode()
+ result = 31 * result + channelId.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt b/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt
index 17d62d3a3..ae40b41f6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt
@@ -2,15 +2,16 @@ package org.koitharu.kotatsu.utils
import android.view.View
import androidx.appcompat.widget.Toolbar
-import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.R as materialR
open class BottomSheetToolbarController(
protected val toolbar: Toolbar,
) : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
- if (newState == BottomSheetBehavior.STATE_EXPANDED) {
+ val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && bottomSheet.top <= 0
+ if (isExpanded) {
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
} else {
toolbar.navigationIcon = null
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt
index 03dd423ea..cedc875fa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt
@@ -10,7 +10,7 @@ import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-class LifecycleAwareServiceConnection private constructor(
+class LifecycleAwareServiceConnection(
private val host: Activity,
) : ServiceConnection, DefaultLifecycleObserver {
@@ -31,19 +31,15 @@ class LifecycleAwareServiceConnection private constructor(
super.onDestroy(owner)
host.unbindService(this)
}
+}
- companion object {
-
- fun bindService(
- host: Activity,
- lifecycleOwner: LifecycleOwner,
- service: Intent,
- flags: Int,
- ): LifecycleAwareServiceConnection {
- val connection = LifecycleAwareServiceConnection(host)
- host.bindService(service, connection, flags)
- lifecycleOwner.lifecycle.addObserver(connection)
- return connection
- }
- }
+fun Activity.bindServiceWithLifecycle(
+ owner: LifecycleOwner,
+ service: Intent,
+ flags: Int
+): LifecycleAwareServiceConnection {
+ val connection = LifecycleAwareServiceConnection(this)
+ bindService(service, connection, flags)
+ owner.lifecycle.addObserver(connection)
+ return connection
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt b/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt
new file mode 100644
index 000000000..e95e0fb96
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt
@@ -0,0 +1,26 @@
+package org.koitharu.kotatsu.utils
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.speech.RecognizerIntent
+import androidx.activity.result.contract.ActivityResultContract
+
+class VoiceInputContract : ActivityResultContract() {
+
+ override fun createIntent(context: Context, input: String?): Intent {
+ val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
+ intent.putExtra(RecognizerIntent.EXTRA_PROMPT, input)
+ return intent
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): String? {
+ return if (resultCode == Activity.RESULT_OK && intent != null) {
+ val matches = intent.getStringArrayExtra(RecognizerIntent.EXTRA_RESULTS)
+ matches?.firstOrNull()
+ } else {
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
index f9f2452ba..ee6b5b30f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
@@ -3,18 +3,25 @@ package org.koitharu.kotatsu.utils.ext
import android.content.Context
import android.content.OperationApplicationException
import android.content.SyncResult
+import android.content.pm.ResolveInfo
import android.database.SQLException
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import android.net.Uri
import android.os.Build
+import androidx.activity.result.ActivityResultLauncher
+import androidx.core.app.ActivityOptionsCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker
+import kotlin.coroutines.resume
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okio.IOException
import org.json.JSONException
import org.koitharu.kotatsu.BuildConfig
-import kotlin.coroutines.resume
val Context.connectivityManager: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -48,6 +55,25 @@ suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching {
setForeground(info)
}.isSuccess
+fun ActivityResultLauncher.resolve(context: Context, input: I): ResolveInfo? {
+ val pm = context.packageManager
+ val intent = contract.createIntent(context, input)
+ return pm.resolveActivity(intent, 0)
+}
+
+fun ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCompat? = null): Boolean {
+ return runCatching {
+ launch(input, options)
+ }.isSuccess
+}
+
+fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
+ coroutineScope.launch {
+ delay(delay)
+ runnable.run()
+ }
+}
+
fun SyncResult.onError(error: Throwable) {
when (error) {
is IOException -> stats.numIoExceptions++
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt
index 04bc82e1d..0ab153da6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt
@@ -3,10 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.collection.ArraySet
import java.util.*
-fun > Array.names() = Array(size) { i ->
- this[i].name
-}
-
fun MutableList.move(sourceIndex: Int, targetIndex: Int) {
if (sourceIndex <= targetIndex) {
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt
index 412ead77c..dd4907134 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt
@@ -3,15 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.CoroutineExceptionHandler
-import org.koitharu.kotatsu.BuildConfig
-
-val IgnoreErrors
- get() = CoroutineExceptionHandler { _, e ->
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
- }
val processLifecycleScope: LifecycleCoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt
index ad1fe8b39..e3cd66b43 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.liveData
+import kotlinx.coroutines.Deferred
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt
index eb38e1d32..317c77c5f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt
@@ -1,7 +1,8 @@
package org.koitharu.kotatsu.utils.progress
+import coil.request.ErrorResult
import coil.request.ImageRequest
-import coil.request.ImageResult
+import coil.request.SuccessResult
import com.google.android.material.progressindicator.BaseProgressIndicator
class ImageRequestIndicatorListener(
@@ -10,9 +11,9 @@ class ImageRequestIndicatorListener(
override fun onCancel(request: ImageRequest) = indicator.hide()
- override fun onError(request: ImageRequest, throwable: Throwable) = indicator.hide()
+ override fun onError(request: ImageRequest, result: ErrorResult) = indicator.hide()
override fun onStart(request: ImageRequest) = indicator.show()
- override fun onSuccess(request: ImageRequest, metadata: ImageResult.Metadata) = indicator.hide()
+ override fun onSuccess(request: ImageRequest, result: SuccessResult) = indicator.hide()
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml
new file mode 100644
index 000000000..f5457e1bd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bookmark.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bookmark_added.xml b/app/src/main/res/drawable/ic_bookmark_added.xml
new file mode 100644
index 000000000..9ebb3889b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bookmark_added.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_voice_input.xml b/app/src/main/res/drawable/ic_voice_input.xml
new file mode 100644
index 000000000..ab46188aa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_voice_input.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml
index 936a0ac6c..6a862073a 100644
--- a/app/src/main/res/layout-w600dp/fragment_details.xml
+++ b/app/src/main/res/layout-w600dp/fragment_details.xml
@@ -157,6 +157,37 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_titles" />
+
+
+
+
@@ -189,6 +220,14 @@
app:showAnimationBehavior="inward"
tools:visibility="visible" />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-w720dp-land/activity_main.xml b/app/src/main/res/layout-w720dp-land/activity_main.xml
index 80a34cfb5..a442269b3 100644
--- a/app/src/main/res/layout-w720dp-land/activity_main.xml
+++ b/app/src/main/res/layout-w720dp-land/activity_main.xml
@@ -31,8 +31,9 @@
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingLeft="16dp"
android:background="@null"
+ android:clipToPadding="false"
+ android:paddingLeft="16dp"
android:paddingRight="16dp"
app:elevation="0dp"
app:liftOnScroll="false">
@@ -61,6 +62,7 @@
android:layout_height="match_parent"
android:background="@null"
android:drawablePadding="16dp"
+ android:layout_marginEnd="4dp"
android:gravity="center_vertical"
android:hint="@string/search_manga"
android:imeOptions="actionSearch"
diff --git a/app/src/main/res/layout/activity_category_edit.xml b/app/src/main/res/layout/activity_category_edit.xml
new file mode 100644
index 000000000..e8b9d8eab
--- /dev/null
+++ b/app/src/main/res/layout/activity_category_edit.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 1366af2c4..6314cf7ef 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -23,6 +23,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
+ android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:stateListAnimator="@null">
@@ -50,13 +51,12 @@
style="@style/Widget.Kotatsu.SearchView"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginEnd="2dp"
+ android:layout_marginEnd="4dp"
android:background="@null"
android:gravity="center_vertical"
android:hint="@string/search_manga"
android:imeOptions="actionSearch"
android:importantForAutofill="no"
- android:paddingBottom="1dp"
android:singleLine="true"
tools:drawableEnd="@drawable/abc_ic_clear_material" />
diff --git a/app/src/main/res/layout/dialog_favorite_categories.xml b/app/src/main/res/layout/dialog_favorite_categories.xml
index 2085c72f7..91cc561aa 100644
--- a/app/src/main/res/layout/dialog_favorite_categories.xml
+++ b/app/src/main/res/layout/dialog_favorite_categories.xml
@@ -5,13 +5,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/list_spacing">
+
+
diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml
index e001bb43d..148179075 100644
--- a/app/src/main/res/layout/fragment_details.xml
+++ b/app/src/main/res/layout/fragment_details.xml
@@ -161,6 +161,37 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_read" />
+
+
+
+
@@ -193,6 +224,14 @@
app:showAnimationBehavior="inward"
tools:visibility="visible" />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_favourites.xml b/app/src/main/res/layout/fragment_favourites.xml
index 0f7e61994..d93d500b3 100644
--- a/app/src/main/res/layout/fragment_favourites.xml
+++ b/app/src/main/res/layout/fragment_favourites.xml
@@ -17,4 +17,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_bookmark.xml b/app/src/main/res/layout/item_bookmark.xml
new file mode 100644
index 000000000..78aab3400
--- /dev/null
+++ b/app/src/main/res/layout/item_bookmark.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_checkable_new.xml b/app/src/main/res/layout/item_checkable_new.xml
index 9ec3eacf1..6e523dad8 100644
--- a/app/src/main/res/layout/item_checkable_new.xml
+++ b/app/src/main/res/layout/item_checkable_new.xml
@@ -1,7 +1,6 @@
\ No newline at end of file
diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml
new file mode 100644
index 000000000..a95b5e211
--- /dev/null
+++ b/app/src/main/res/layout/preference_toggle_header.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/opt_favourites_bs.xml b/app/src/main/res/menu/opt_favourites_bs.xml
deleted file mode 100644
index 56e7d5723..000000000
--- a/app/src/main/res/menu/opt_favourites_bs.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/opt_favourites_list.xml b/app/src/main/res/menu/opt_favourites_list.xml
new file mode 100644
index 000000000..269569df7
--- /dev/null
+++ b/app/src/main/res/menu/opt_favourites_list.xml
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/opt_reader_bottom.xml b/app/src/main/res/menu/opt_reader_bottom.xml
index b56029f73..569bffc11 100644
--- a/app/src/main/res/menu/opt_reader_bottom.xml
+++ b/app/src/main/res/menu/opt_reader_bottom.xml
@@ -5,6 +5,20 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
+
+
+
+
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/popup_category.xml b/app/src/main/res/menu/popup_category.xml
index 1c4fc96d2..50c1313be 100644
--- a/app/src/main/res/menu/popup_category.xml
+++ b/app/src/main/res/menu/popup_category.xml
@@ -7,20 +7,7 @@
android:title="@string/remove" />
-
- -
-
-
-
-
-
-
-
+ android:id="@+id/action_edit"
+ android:title="@string/edit" />
\ No newline at end of file
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index cbd9de9a7..bada7be01 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -101,7 +101,6 @@
Паведамленні
Уключана %1$d з %2$d
Новыя главы
- Апавяшчаць пра абнаўленні мангі, якую вы чытаеце
Спампаваць
Чытаць з пачатку
Перазапусціць
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 42fb3a9ee..a8c8d8feb 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -46,7 +46,6 @@
Aktualisierungsfeed gelöscht
Aktualisierungsfeed löschen
Aktualisierungen
- Über Aktualisierungen von Manga benachrichtigen, die du liest
Benachrichtigung anzeigen, wenn eine Aktualisierung verfügbar ist
Anwendungsaktualisierung ist verfügbar
Automatisch nach Aktualisierungen suchen
@@ -278,4 +277,5 @@
Hilft, das Blockieren Ihrer IP-Adresse zu vermeiden
Die Kapitel werden im Hintergrund entfernt. Das kann einige Zeit dauern
Ausblenden
+ Neue Manga-Quellen sind verfügbar
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 9cde0edc3..ef974cffd 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -101,7 +101,6 @@
Notificaciones
Activado %1$d de %2$d
Nuevos capítulos
- Notificar sobre las actualizaciones del manga que estás leyendo
Descargar
Leer desde el principio
Reiniciar
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 69f9c193e..2afc820b9 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -118,7 +118,6 @@
Käynnistä uudelleen
Lue alusta
Lataa
- Ilmoita lukemastasi mangan päivityksistä
Uusia lukuja
Käytössä %1$d / %2$d
Ilmoitukset
@@ -278,4 +277,5 @@
Oletko varma, että haluat ladata kaikki valitut mangat kaikkine lukuineen\? Tämä toiminto voi kuluttaa paljon liikennettä ja tallennustilaa
Tallennettujen mangojen käsittely
Piilota
+ Uusia mangalähteitä on saatavilla
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 958794036..a70284d3f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -112,7 +112,6 @@
Paramètres des notifications
Lire depuis le début
Télécharger
- Avertir des mises à jour des mangas que vous lisez
Nouveaux chapitres
%1$d de %2$d activé(s)
Notifications
@@ -278,4 +277,5 @@
Les chapitres seront supprimés en arrière-plan. Cela peut prendre un certain temps
Traitement des mangas sauvegardés
Masquer
+ De nouvelles sources de mangas sont disponibles
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index e6196111b..65fad3ab8 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -154,7 +154,6 @@
Riavvia
Leggi dall\'inizio
Scarica
- Notifica gli aggiornamenti dei manga che stai leggendo
Nuovi capitoli
Abilitato %1$d di %2$d
Notifiche
@@ -278,4 +277,5 @@
I capitoli saranno rimossi in sfondo. Può richiedere un po\' di tempo
Aiuta ad evitare il blocco del tuo indirizzo IP
Nascondi
+ Sono disponibili nuove fonti di manga
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index d6db95de8..c8066422c 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -143,7 +143,6 @@
検索履歴をクリア
外部ストレージ
Kotatsuの新しい更新が利用可能です
- あなたが読んでいる漫画の更新について通知
ここは空っぽです…
カテゴリーを使用してお気に入りを整理できます。 «+»を押してカテゴリーを作成出来ます
空のカテゴリー
@@ -278,4 +277,5 @@
並列ダウンロード
チャプターはバックグラウンドで削除されます。時間がかかる場合があります
隠す
+ 新しいマンガソースが利用可能になりました
\ No newline at end of file
diff --git a/app/src/main/res/values-large/themes.xml b/app/src/main/res/values-large/themes.xml
new file mode 100644
index 000000000..56e1ed965
--- /dev/null
+++ b/app/src/main/res/values-large/themes.xml
@@ -0,0 +1,34 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 74c07b980..41444062e 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -54,7 +54,6 @@
Fjern «%s»-kategorien fra favorittene\?
\nAlle mangaer i den vil bli tapt.
Programomstart
- Gi merknad om oppdateringer av det du leser
%1$d av %2$d påskrudd
Denne mangaen har %s. Lagre hele\?
Åpne i nettleser
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 32910e0a3..2af8ed7e3 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -232,7 +232,6 @@
Limpar cache de miniaturas
Verifique se há novas versões do aplicativo
Categorias favoritas
- Notificar sobre atualizações de mangá que você está lendo
Remover a categoria “%s” dos seus favoritos\?
\nTodos os mangás nela serão perdidos.
Nenhuma atualização disponível
@@ -278,4 +277,5 @@
Processamento de mangá salvo
Os capítulos serão removidos em segundo plano. Pode levar algum tempo
Downloads paralelos
+ Novas fontes de mangá estão disponíveis
\ No newline at end of file
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index b23edc825..d3794d77a 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -83,7 +83,6 @@
Salve
Notificações
Novos capítulos
- Notifique sobre atualizações do mangá que está lendo
Download
Ler desde o início
Reiniciar
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 41dad2021..0b4fa423e 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -101,7 +101,6 @@
Уведомления
Включено %1$d из %2$d
Новые главы
- Уведомлять об обновлении манги, которую Вы читаете
Загрузить
Читать с начала
Перезапустить
@@ -278,4 +277,12 @@
Главы будут удалены в фоновом режиме. Это может занять какое-то время
Скрыть
Доступны новые источники манги
+ Проверять новые главы и уведомлять о них
+ Вы будете получать уведомления об обновлении манги, которую Вы читаете
+ Вы не будете получать уведомления, но новые главы будут отображаться в списке
+ Включить уведомления
+ Название
+ Изменить
+ Изменить категорию
+ Нет категорий избранного
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 76cfa061a..264ad6c1c 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -105,7 +105,6 @@
Ladda ned
Aviseringsinställningar
LED-indikator
- Avisera om uppdateringar på manga du läser
Läs från början
Starta om
Aviseringsljud
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index ff48edbc1..0580f512c 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -234,7 +234,6 @@
Öntanımlı
Bir ad girmelisiniz
%s üzerinde oturum açma desteklenmiyor
- Okunan manga güncellemeleri hakkında bildirimde bulun
Daha fazla oku
Bazı aygıtların arka plan görevlerini bozabilecek farklı sistem davranışları vardır.
Ekran görüntüsü politikası
@@ -278,4 +277,5 @@
IP adresinizin engellenmesinden kaçınmanıza yardımcı olur
Kaydedilen manga işleme
Gizle
+ Yeni manga kaynakları var
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/plurals.xml b/app/src/main/res/values-uk/plurals.xml
new file mode 100644
index 000000000..0ced943b7
--- /dev/null
+++ b/app/src/main/res/values-uk/plurals.xml
@@ -0,0 +1,51 @@
+
+
+
+ - %1$d новий розділ
+ - %1$d нових розділи
+ - %1$d нових розділів
+ - %1$d нових розділів
+
+
+ - %1$d хвилину тому
+ - %1$d хвилини тому
+ - %1$d хвилин тому
+ - %1$d хвилин тому
+
+
+ - Всього %1$d сторінка
+ - Всього %1$d сторінки
+ - Всього %1$d сторінок
+ - Всього %1$d сторінок
+
+
+ - %1$d годину тому
+ - %1$d години тому
+ - %1$d годин тому
+ - %1$d годин тому
+
+
+ - %1$d день тому
+ - %1$d дні тому
+ - %1$d днів тому
+ - %1$d днів тому
+
+
+ - %1$d елемент
+ - %1$d елементи
+ - %1$d елементів
+ - %1$d елементів
+
+
+ - %1$d розділ із %2$d
+ - %1$d розділи з %2$d
+ - %1$d розділів із %2$d
+ - %1$d розділів із %2$d
+
+
+ - %1$d розділ
+ - %1$d розділи
+ - %1$d розділів
+ - %1$d розділів
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
new file mode 100644
index 000000000..e476c3a7b
--- /dev/null
+++ b/app/src/main/res/values-uk/strings.xml
@@ -0,0 +1,282 @@
+
+
+ Дочекайтеся завершення завантаження…
+ Видалити
+ Нічого не знайдено
+ Додати це до улюблених
+ Очистити історію
+ Історії ще немає
+ Додати
+ Зберегти
+ Локальне сховище
+ Не вдалося підключитися до Інтернету
+ Деталі
+ Спробуйте ще раз
+ Відкрити меню
+ Улюблених ще немає
+ Нова категорія
+ Введіть назву категорії
+ Завантажено
+ Уподобання
+ Історія
+ Сталася помилка
+ Розділи
+ Список
+ Детальний список
+ Режим списку
+ Налаштування
+ Віддалені джерела
+ Завантаження…
+ Обчислення…
+ Розділ %1$d із %2$d
+ Закрити
+ Читати
+ Таблиця
+ Поділитися
+ Створити ярлик…
+ Поділитися %s
+ Пошук
+ Пошук манґи
+ Обробка…
+ Ім\'я
+ Популярна
+ Оновлена
+ Нова
+ Рейтинг
+ Порядок сортування
+ Фільтр
+ Тема
+ Світла
+ Темна
+ Сторінки
+ Очистити всю історію читання перманентно\?
+ Видалити
+ \"%s\" видалено з історії
+ \"%s\" видалено з локального сховища
+ Зберегти сторінку
+ Збережено
+ Поділитись зображенням
+ Ця операція не підтримується
+ Виберіть файл ZIP або CBZ.
+ Немає опису
+ Історія та кеш
+ Очистити кеш сторінок
+ Кеш
+ Б|кБ|МБ|ГБ|ТБ
+ Стандартний
+ Манхва
+ Режим читання
+ Розмір сітки
+ Пошук по %s
+ Видалити манґу
+ Видалити \"%s\" з пристрою перманентно\?
+ Налаштування читача
+ Перегортання сторінок
+ Кнопки гучності
+ Скасування…
+ Помилка
+ Очистити кеш мініатюр
+ Очистити історію пошуку
+ Очищено
+ Тільки жести
+ Внутрішнє сховище
+ Зовнішнє сховище
+ Домен
+ Перевірити наявність нових версій додатка
+ Доступна нова версія додатка
+ Ця манґа має %s. Зберегти все це\?
+ Зберегти
+ Сповіщення
+ Увімкнено %1$d з %2$d
+ Нові розділи
+ Повідомляти про оновлення манґи, яку Ви читаєте
+ Завантажити
+ Читати з початку
+ Перезавантажити
+ Вібрація
+ Улюблені категорії
+ Вилучити категорію \"%s\" зі своїх уподобань\?
+\nВся манґа в ній буде втрачена.
+ Видалити
+ Тут якось пусто…
+ Спробуйте переформулювати запит.
+ Те, що ви читаєте, буде показано тут
+ Знайдіть, що читати, у бічному меню.
+ Спочатку збережіть щось
+ Збережіть його з онлайн-джерела або імпортуйте файли.
+ Полиця
+ Недавні
+ Анімація перегортання
+ Папка для завантажень
+ Інше сховище
+ Готово
+ Усі улюблені
+ Порожня категорія
+ Прочитати пізніше
+ Оновлення
+ Схожі
+ Нова версія: %s
+ Розмір: %s
+ Очікування мережі…
+ Очистити стрічку оновлень
+ Очищено
+ Повернути екран
+ Оновити
+ Оновлення скоро почнеться
+ Стежити за оновленнями
+ Не перевіряти
+ Неправильний пароль
+ Захистити додаток
+ Запитувати пароль під час запуску Kotatsu
+ Повторіть пароль
+ Паролі не співпадають
+ Про програму
+ Версія %s
+ Перевірити наявність оновлень
+ Перевірка наявності оновлень…
+ Не вдалося перевірити оновлення
+ Немає доступних оновлень
+ Віддавати перевагу читанню справа наліво (←)
+ Нова категорія
+ Режим масштабування
+ Вмістити в екран
+ Підігнати по висоті
+ підігнати по ширині
+ Вихідний розмір
+ Чорна
+ Споживає менше енергії на екранах AMOLED
+ Потрібен перезапуск
+ Резервне копіювання та відновлення
+ Відновлено
+ Підготовка…
+ Створити проблему на GitHub
+ Файл не знайдено
+ Дані відновлено, але є деякі помилки
+ Ви можете створити резервну копію своєї історії та уподобань і відновити їх
+ Тільки що
+ Торкніться, щоб спробувати ще раз
+ Обраний режим буде запам\'ятован для цієї манги
+ Потрібна CAPTCHA
+ Пройти
+ Очистити кукі
+ Всі кукі були видалені
+ Очистити стрічку
+ Перевірити нові розділи
+ В зворотньому порядку
+ Увійти
+ Увійдіть, щоб переглянути цей вміст
+ За замовчуванням: %s
+ ...і ще %1$d
+ Далі
+ Введіть пароль для запуску програми
+ Підтвердити
+ Пароль має містити 4 символи або більше
+ Пошук лише на %s
+ Ласкаво просимо
+ Резервна копія збережена
+ Докладніше
+ У черзі
+ Немає активних завантажень
+ Допомогти з перекладом програми
+ Переклад
+ Тема на 4PDA
+ Авторизація виконана
+ Вхід на %s не підтримується
+ Ви вийдете з усіх джерел
+ Завершена
+ Триває
+ Формат дати
+ Виключити NSFW манґу з історії
+ Ви повинні ввести ім’я
+ Показувати номери сторінок
+ Включені джерела
+ Застосовує тему програми, засновану на палітрі кольорів шпалер на пристрої
+ Імпорт манґи: %1$d з %2$d
+ Політика щодо знімків екрана
+ Дозволити
+ Пропонувати манґу на основі ваших уподобань
+ Усі дані аналізуються локально на цьому пристрої. Передача ваших персональних даних у будь-які сервіси не здійснюється
+ Почніть читати манґу, і ви отримаєте персоналізовані пропозиції
+ Увімкнено
+ Вимкнено
+ Скинути фільтр
+ Знайти жанр
+ Виберіть мови, якими ви хочете читати манґу. Це можливо змінити пізніше в налаштуваннях.
+ Тільки по Wi-Fi
+ Попереднє завантаження сторінок
+ Ви увійшли як %s
+ 18+
+ Різні мови
+ Знайти розділ
+ Немає розділів у цій манзі
+ %1$s%%
+ Зміст
+ Оновлення пропозицій
+ Видалити вибрані елементи з пристрою назавжди\?
+ Видалення завершено
+ Ви впевнені, що хочете завантажити всю вибрану манґу з усіма її розділами\? Це може споживати багато трафіку та пам’яті
+ Завантажувати паралельно
+ Сповільнення завантаження
+ Обробка збереженої манґи
+ Приховати
+ Доступні нові джерела манґи
+ Закрити меню
+ Завантаження…
+ Очистити
+ Завантаження
+ Як в системі
+ Завантажте або прочитайте цей відсутній розділ онлайн.
+ Розділ відсутній
+ Зворотній зв\'язок
+ Жанри
+ За замовчуванням
+ Завжди
+ Продовжити
+ Імпорт
+ Натискання по краях
+ Попередження
+ Це може призвести до витрати великої кількості трафіку
+ Більше не питати
+ Налаштування сповіщень
+ Перейменувати
+ Показувати сповіщення, якщо доступна нова версія
+ Відкрити у веб-браузері
+ Недоступно
+ Немає доступного сховища
+ Нові розділи того, що ви читаєте, показано тут
+ Результати пошуку
+ Введіть пароль
+ Звук сповіщень
+ Світлодіодний індикатор
+ Категорії…
+ Ви можете використовувати категорії для впорядкування своїх уподобань. Натисніть «+», щоб створити категорію
+ Учора
+ Справа наліво (←)
+ Режим читання можна налаштувати окремо для кожної серії
+ Створити резервну копію
+ Відновити з резервної копії
+ Всі дані були відновлені
+ Групувати
+ Сьогодні
+ Без звуку
+ Давно
+ Перевірка наявності нових розділів: %1$d з %2$d
+ Очистити всю історію оновлень назавжди\?
+ Деякі пристрої мають різну поведінку системи, що може порушити фонові завдання.
+ Видалити всі останні пошукові запити назавжди\?
+ Інше
+ Доступні джерела
+ Динамічна тема
+ Блок на NSFW
+ Завжди блокувати
+ Пропозиції
+ Увімкнути пропозиції
+ Не пропонувати NSFW манґу
+ Не вдалося завантажити список жанрів
+ Ніколи
+ Зовнішній вигляд
+ Виключити жанри
+ Укажіть жанри, які ви не хочете бачити в пропозиціях
+ Допомагає уникнути блокування вашої IP-адреси
+ Розділи будуть видалені у фоновому режимі. Це може зайняти деякий час
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml
new file mode 100644
index 000000000..370045cf3
--- /dev/null
+++ b/app/src/main/res/values-v27/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 03bed46af..176b70687 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -34,4 +34,15 @@
- @string/only_using_wifi
- @string/never
+
+ - @string/disabled
+ - Google
+ - CloudFlare
+ - AdGuard
+
+
+ - @string/standard
+ - @string/right_to_left
+ - @string/webtoon
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index c14ddff76..348cff5e0 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -26,8 +26,7 @@
-
-
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 282d15034..20debe81b 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -13,6 +13,8 @@
2dp
86dp
120dp
+ 120dp
+ 4dp
62dp
120dp
48dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 19b95af7e..a0e4737a4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,242 +1,239 @@
Kotatsu
- Close menu
- Open menu
- Local storage
- Favourites
- History
- An error occurred
- Could not connect to the Internet
- Details
- Chapters
- List
- Detailed list
- Grid
- List mode
- Settings
- Remote sources
- Loading…
- Computing…
- Chapter %1$d of %2$d
- Close
- Try again
- Clear history
- Nothing found
- No history yet
- Read
- No favourites yet
- Favourite this
- New category
- Add
- Enter category name
- Save
- Share
- Create shortcut…
- Share %s
- Search
- Search manga
- Downloading…
- Processing…
- Downloaded
- Downloads
- Name
- Popular
- Updated
- Newest
- Rating
- Sorting order
- Filter
- Theme
- Light
- Dark
- Follow system
- Pages
- Clear
- Clear all reading history permanently?
- Remove
- \"%s\" removed from history
- \"%s\" deleted from local storage
- Wait for loading to finish…
- Save page
- Saved
- Share image
- Import
- Delete
- This operation is not supported
- Either pick a ZIP or CBZ file.
- No description
- History and cache
- Clear page cache
- Cache
- B|kB|MB|GB|TB
- Standard
- Webtoon
- Read mode
- Grid size
- Search on %s
- Delete manga
- Delete \"%s\" from device permanently?
- Reader settings
- Switch pages
- Edge taps
- Volume buttons
- Continue
- Warning
- This may transfer a lot of data
- Don\'t ask again
- Cancelling…
- Error
- Clear thumbnails cache
- Clear search history
- Cleared
- Gestures only
- Internal storage
- External storage
- Domain
- Check for new versions of the app
- A new version of the app is available
- Show notification if a new version is available
- Open in web browser
- This manga has %s. Save all of it?
- Save
- Notifications
- %1$d of %2$d on
- New chapters
- Notify about updates of manga you are reading
- Download
- Read from start
- Restart
- Notifications settings
- Notification sound
- LED indicator
- Vibration
- Favourite categories
- Categories…
- Rename
- Remove the \"%s\" category from your favourites? \nAll manga in it will be lost.
- Remove
- It\'s kind of empty here…
- You can use categories to organize your favourites. Press «+» to create a category
- Try to reformulate the query.
- What you read will be displayed here
- Find what to read in side menu.
- Save something first
- Save it from online sources or import files.
- Shelf
- Recent
- Page animation
- Folder for downloads
- Not available
- No available storage
- Other storage
+ Close menu
+ Open menu
+ Local storage
+ Favourites
+ History
+ An error occurred
+ Could not connect to the Internet
+ Details
+ Chapters
+ List
+ Detailed list
+ Grid
+ List mode
+ Settings
+ Remote sources
+ Loading…
+ Computing…
+ Chapter %1$d of %2$d
+ Close
+ Try again
+ Clear history
+ Nothing found
+ No history yet
+ Read
+ No favourites yet
+ Favourite this
+ New category
+ Add
+ Enter category name
+ Save
+ Share
+ Create shortcut…
+ Share %s
+ Search
+ Search manga
+ Downloading…
+ Processing…
+ Downloaded
+ Downloads
+ Name
+ Popular
+ Updated
+ Newest
+ Rating
+ Sorting order
+ Filter
+ Theme
+ Light
+ Dark
+ Follow system
+ Pages
+ Clear
+ Clear all reading history permanently?
+ Remove
+ \"%s\" removed from history
+ \"%s\" deleted from local storage
+ Wait for loading to finish…
+ Save page
+ Saved
+ Share image
+ Import
+ Delete
+ This operation is not supported
+ Either pick a ZIP or CBZ file.
+ No description
+ History and cache
+ Clear page cache
+ Cache
+ B|kB|MB|GB|TB
+ Standard
+ Webtoon
+ Read mode
+ Grid size
+ Search on %s
+ Delete manga
+ Delete \"%s\" from device permanently?
+ Reader settings
+ Switch pages
+ Edge taps
+ Volume buttons
+ Continue
+ Warning
+ This may transfer a lot of data
+ Don\'t ask again
+ Cancelling…
+ Error
+ Clear thumbnails cache
+ Clear search history
+ Cleared
+ Gestures only
+ Internal storage
+ External storage
+ Domain
+ Check for new versions of the app
+ A new version of the app is available
+ Show notification if a new version is available
+ Open in web browser
+ This manga has %s. Save all of it?
+ Save
+ Notifications
+ %1$d of %2$d on
+ New chapters
+ Download
+ Read from start
+ Restart
+ Notifications settings
+ Notification sound
+ LED indicator
+ Vibration
+ Favourite categories
+ Categories…
+ Rename
+ Remove the \"%s\" category from your favourites? \nAll manga in it will be lost.
+ Remove
+ It\'s kind of empty here…
+ You can use categories to organize your favourites. Press «+» to create a category
+ Try to reformulate the query.
+ What you read will be displayed here
+ Find what to read in side menu.
+ Save something first
+ Save it from online sources or import files.
+ Shelf
+ Recent
+ Page animation
+ Folder for downloads
+ Not available
+ No available storage
+ Other storage
Done
- All favourites
- Empty category
- Read later
- Updates
- New chapters of what you are reading is shown here
- Search results
- Related
- New version: %s
- Size: %s
- Waiting for network…
- Clear updates feed
- Cleared
- Rotate screen
- Update
- Feed update will start soon
- Look for updates
- Don\'t check
- Enter password
- Wrong password
- Protect the app
- Ask for password when starting Kotatsu
- Repeat the password
- Mismatching passwords
- About
- Version %s
- Check for updates
- Checking for updates…
- Could not look for updates
- No updates available
- Right-to-left (←)
- Prefer right-to-left (←) reader
- Reading mode can be set up separately for each series
- New category
- Scale mode
- Fit center
- Fit to height
- Fit to width
- Keep at start
- Black
- Uses less power on AMOLED screens
- Restart required
- Backup and restore
- Create data backup
- Restore from backup
- Restored
- Preparing…
- Create issue on GitHub
- File not found
- All data was restored
- The data was restored, but there are errors
- You can create backup of your history and favourites and restore it
- Just now
- Yesterday
- Long ago
- Group
- Today
- Tap to try again
- The chosen configuration will be remembered for this manga
- Silent
- CAPTCHA required
- Solve
- Clear cookies
- All cookies were removed
- Checking for new chapters: %1$d of %2$d
- Clear feed
- Clear all update history permanently?
- Check for new chapters
- Reverse
- Sign in
- Sign in to view this content
- Default: %s
- …and %1$d more
- Next
- Enter a password to start the app with
- Confirm
- The password must be 4 characters or more
+ All favourites
+ Empty category
+ Read later
+ Updates
+ New chapters of what you are reading is shown here
+ Search results
+ Related
+ New version: %s
+ Size: %s
+ Waiting for network…
+ Clear updates feed
+ Cleared
+ Rotate screen
+ Update
+ Feed update will start soon
+ Look for updates
+ Don\'t check
+ Enter password
+ Wrong password
+ Protect the app
+ Ask for password when starting Kotatsu
+ Repeat the password
+ Mismatching passwords
+ About
+ Version %s
+ Check for updates
+ Checking for updates…
+ Could not look for updates
+ No updates available
+ Right-to-left (←)
+ New category
+ Scale mode
+ Fit center
+ Fit to height
+ Fit to width
+ Keep at start
+ Black
+ Uses less power on AMOLED screens
+ Restart required
+ Backup and restore
+ Create data backup
+ Restore from backup
+ Restored
+ Preparing…
+ Create issue on GitHub
+ File not found
+ All data was restored
+ The data was restored, but there are errors
+ You can create backup of your history and favourites and restore it
+ Just now
+ Yesterday
+ Long ago
+ Group
+ Today
+ Tap to try again
+ The chosen configuration will be remembered for this manga
+ Silent
+ CAPTCHA required
+ Solve
+ Clear cookies
+ All cookies were removed
+ Checking for new chapters: %1$d of %2$d
+ Clear feed
+ Clear all update history permanently?
+ Check for new chapters
+ Reverse
+ Sign in
+ Sign in to view this content
+ Default: %s
+ …and %1$d more
+ Next
+ Enter a password to start the app with
+ Confirm
+ The password must be 4 characters or more
Search only on %s
- Remove all recent search queries permanently?
- Other
- Welcome
- Backup saved
- Some devices have different system behavior, which may break background tasks.
- Read more
- Queued
- No active downloads
- Download or read this missing chapter online.
- The chapter is missing
- Translate this app
- Translation
- Feedback
- Topic on 4PDA
+ Remove all recent search queries permanently?
+ Other
+ Welcome
+ Backup saved
+ Some devices have different system behavior, which may break background tasks.
+ Read more
+ Queued
+ No active downloads
+ Download or read this missing chapter online.
+ The chapter is missing
+ Translate this app
+ Translation
+ Feedback
+ Topic on 4PDA
Authorized
- Logging in on %s is not supported
- You will be logged out from all sources
- Genres
- Finished
- Ongoing
- Date format
- Default
- Exclude NSFW manga from history
- You must enter a name
- Numbered pages
- Used sources
- Available sources
- Dynamic theme
- Applies a theme created on the color scheme of your wallpaper
+ Logging in on %s is not supported
+ You will be logged out from all sources
+ Genres
+ Finished
+ Ongoing
+ Date format
+ Default
+ Exclude NSFW manga from history
+ You must enter a name
+ Numbered pages
+ Used sources
+ Available sources
+ Dynamic theme
+ Applies a theme created on the color scheme of your wallpaper
Importing manga: %1$d of %2$d
Screenshot policy
Allow
@@ -287,4 +284,23 @@
Enter your email to continue
Hide
New manga sources are available
+ Check for new chapters and notify about it
+ You will receive notifications about updates of manga you are reading
+ You will not receive notifications but new chapters will be highlighted in the lists
+ Enable notifications
+ Name
+ Edit
+ Edit category
+ No favourite categories
+ Add bookmark
+ Remove bookmark
+ Bookmarks
+ Bookmark removed
+ Bookmark added
+ Undo
+ Removed from history
+ DNS over HTTPS
+ Default mode
+ Autodetect reader mode
+ Automatically detect if manga is webtoon
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 2333f5d68..7f96a0bef 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -22,8 +22,9 @@
-
@@ -39,6 +40,7 @@
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 12ccac10f..f6addbe8f 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -71,7 +71,8 @@
- @style/Widget.Kotatsu.RecyclerView
- @style/Widget.Kotatsu.ListItemTextView
-
+
+ - @style/TextAppearance.Kotatsu.Menu
- ?attr/textAppearanceBodyLarge
- @style/TextAppearance.Kotatsu.Preference.Secondary
@@ -80,6 +81,8 @@
+
+
diff --git a/app/src/main/res/xml/pref_content.xml b/app/src/main/res/xml/pref_content.xml
index 53fee4124..7b48494ea 100644
--- a/app/src/main/res/xml/pref_content.xml
+++ b/app/src/main/res/xml/pref_content.xml
@@ -14,6 +14,12 @@
android:persistent="false"
android:title="@string/suggestions" />
+
+
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml
index e152d7054..42e46b8ad 100644
--- a/app/src/main/res/xml/pref_reader.xml
+++ b/app/src/main/res/xml/pref_reader.xml
@@ -3,17 +3,22 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+ android:defaultValue="true"
+ android:key="reader_mode_detect"
+ android:summary="@string/detect_reader_mode_summary"
+ android:title="@string/detect_reader_mode" />
+
+
-
+