diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 203df6bfe..03f83f88f 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,2 @@
+ko_fi: xtimms
custom: ["https://yoomoney.ru/to/410012543938752"]
-ko_fi: koitharu
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index d6504c977..dd185e22b 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -4,6 +4,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a97f2a74c..599df4214 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Download APK directly from GitHub:
* Notifications about new chapters with updates feed
* Shikimori integration (manga tracking)
* Password/fingerprint protect access to the app
-* History and favourites synchronization across devices (coming soon)
+* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
### Screenshots
diff --git a/app/build.gradle b/app/build.gradle
index b6caf610d..71d5901ea 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
- versionCode 500
- versionName '4.0'
+ versionCode 503
+ versionName '4.0.3'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -83,15 +83,15 @@ afterEvaluate {
}
}
dependencies {
- implementation('com.github.KotatsuApp:kotatsu-parsers:5cb953eb86') {
+ implementation('com.github.KotatsuApp:kotatsu-parsers:bf8a1f3db2') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0'
- implementation 'androidx.activity:activity-ktx:1.6.0'
- implementation 'androidx.fragment:fragment-ktx:1.5.3'
+ implementation 'androidx.activity:activity-ktx:1.6.1'
+ implementation 'androidx.fragment:fragment-ktx:1.5.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
@@ -103,7 +103,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
- implementation 'com.google.android.material:material:1.7.0-rc01'
+ implementation 'com.google.android.material:material:1.7.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
@@ -123,9 +123,9 @@ dependencies {
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
- implementation 'io.coil-kt:coil-base:2.2.1'
- implementation 'io.coil-kt:coil-svg:2.2.1'
- implementation 'com.github.KotatsuApp:subsampling-scale-image-view:0ff0278f0f'
+ implementation 'io.coil-kt:coil-base:2.2.2'
+ implementation 'io.coil-kt:coil-svg:2.2.2'
+ implementation 'com.github.KotatsuApp:subsampling-scale-image-view:f8a38b08fe'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'ch.acra:acra-http:5.9.6'
@@ -134,7 +134,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2'
- testImplementation 'org.json:json:20220320'
+ testImplementation 'org.json:json:20220924'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:runner:1.4.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3faa30c69..4ca5eaf80 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,7 @@
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -108,8 +109,7 @@
+ android:label="@string/manga_shelf">
@@ -127,18 +127,18 @@
+ android:launchMode="singleTop" />
-
+
+
:
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
val isAmoled = settings.isAmoledTheme
val isDynamic = settings.isDynamicTheme
- // TODO support DialogWhenLarge theme
when {
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/DefaultActivityLifecycleCallbacks.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/DefaultActivityLifecycleCallbacks.kt
new file mode 100644
index 000000000..dd83e4dc7
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/DefaultActivityLifecycleCallbacks.kt
@@ -0,0 +1,22 @@
+package org.koitharu.kotatsu.base.ui
+
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
+import android.os.Bundle
+
+interface DefaultActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
+
+ override fun onActivityStarted(activity: Activity) = Unit
+
+ override fun onActivityResumed(activity: Activity) = Unit
+
+ override fun onActivityPaused(activity: Activity) = Unit
+
+ override fun onActivityStopped(activity: Activity) = Unit
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
+
+ override fun onActivityDestroyed(activity: Activity) = Unit
+}
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
index 650e816c5..19d1d5661 100644
--- 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
@@ -6,7 +6,7 @@ import android.view.View.OnLongClickListener
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
class AdapterDelegateClickListenerAdapter(
- private val adapterDelegate: AdapterDelegateViewBindingViewHolder,
+ private val adapterDelegate: AdapterDelegateViewBindingViewHolder,
private val clickListener: OnListItemClickListener,
) : OnClickListener, OnLongClickListener {
@@ -17,4 +17,4 @@ class AdapterDelegateClickListenerAdapter(
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/ActivityRecreationHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt
index 985205ed6..e50c92479 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt
@@ -1,14 +1,14 @@
package org.koitharu.kotatsu.base.ui.util
import android.app.Activity
-import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
-import java.util.*
+import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
+import java.util.WeakHashMap
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallbacks {
+class ActivityRecreationHandle @Inject constructor() : DefaultActivityLifecycleCallbacks {
private val activities = WeakHashMap()
@@ -16,16 +16,6 @@ class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallback
activities[activity] = Unit
}
- override fun onActivityStarted(activity: Activity) = Unit
-
- override fun onActivityResumed(activity: Activity) = Unit
-
- override fun onActivityPaused(activity: Activity) = Unit
-
- override fun onActivityStopped(activity: Activity) = Unit
-
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
-
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt
index 1b9c8109c..459ac9e8b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt
@@ -18,8 +18,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
-import java.util.concurrent.TimeUnit
-import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import okhttp3.CookieJar
import okhttp3.OkHttpClient
@@ -40,9 +38,12 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.sync.domain.SyncController
+import org.koitharu.kotatsu.utils.IncognitoModeIndicator
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.image.CoilImageGetter
import org.koitharu.kotatsu.widget.WidgetUpdater
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -152,9 +153,11 @@ interface AppModule {
fun provideActivityLifecycleCallbacks(
appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle,
+ incognitoModeIndicator: IncognitoModeIndicator,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper,
activityRecreationHandle,
+ incognitoModeIndicator,
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt
index 5960cb5e5..467fcab9a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
fun TagEntity.toMangaTag() = MangaTag(
key = this.key,
title = this.title.toTitleCase(),
- source = MangaSource(this.source) ?: MangaSource.DUMMY,
+ source = MangaSource(this.source),
)
fun Collection.toMangaTags() = mapToSet(TagEntity::toMangaTag)
@@ -28,7 +28,7 @@ fun MangaEntity.toManga(tags: Set) = Manga(
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
- source = MangaSource(this.source) ?: MangaSource.DUMMY,
+ source = MangaSource(this.source),
tags = tags,
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt
index 27f73e19f..8ae3565e5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt
@@ -6,9 +6,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
@@ -20,6 +17,9 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.isSuccess
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
class ExceptionResolver private constructor(
private val activity: FragmentActivity?,
@@ -49,6 +49,7 @@ class ExceptionResolver private constructor(
openInBrowser(e.url)
false
}
+
else -> false
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt
index 22c2319bf..341796c1e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt
@@ -1,18 +1,17 @@
package org.koitharu.kotatsu.core.model
-import java.util.*
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
+import java.util.Locale
fun MangaSource.getLocaleTitle(): String? {
val lc = Locale(locale ?: return null)
return lc.getDisplayLanguage(lc).toTitleCase(lc)
}
-@Suppress("FunctionName")
-fun MangaSource(name: String): MangaSource? {
+fun MangaSource(name: String): MangaSource {
MangaSource.values().forEach {
if (it.name == name) return it
}
- return null
-}
\ No newline at end of file
+ return MangaSource.DUMMY
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt
index 159ef8a4f..8450028e9 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt
@@ -7,10 +7,12 @@ import android.net.NetworkRequest
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.onSuccess
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
import javax.inject.Inject
@@ -26,7 +28,8 @@ class NetworkStateObserver @Inject constructor(
override val replayCache: List
get() = listOf(value)
- override var value: Boolean = connectivityManager.isNetworkAvailable
+ override val value: Boolean
+ get() = connectivityManager.isNetworkAvailable
override suspend fun collect(collector: FlowCollector): Nothing {
collector.emit(value)
@@ -35,6 +38,13 @@ class NetworkStateObserver @Inject constructor(
}
}
+ suspend fun awaitForConnection(): Unit {
+ if (value) {
+ return
+ }
+ first { it }
+ }
+
private fun observeImpl() = callbackFlow {
val request = NetworkRequest.Builder().build()
val callback = FlowNetworkCallback(this)
@@ -44,9 +54,12 @@ class NetworkStateObserver @Inject constructor(
}
}
- inner class FlowNetworkCallback(
+ private inner class FlowNetworkCallback(
private val producerScope: ProducerScope,
) : NetworkCallback() {
+
+ private var prevValue = value
+
override fun onAvailable(network: Network) = update()
override fun onLost(network: Network) = update()
@@ -55,9 +68,10 @@ class NetworkStateObserver @Inject constructor(
private fun update() {
val newValue = connectivityManager.isNetworkAvailable
- if (value != newValue) {
- value = newValue
- producerScope.trySendBlocking(newValue)
+ if (newValue != prevValue) {
+ producerScope.trySendBlocking(newValue).onSuccess {
+ prevValue = newValue
+ }
}
}
}
diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/parser/MangaParser.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaParser.kt
similarity index 100%
rename from app/src/debug/java/org/koitharu/kotatsu/core/parser/MangaParser.kt
rename to app/src/main/java/org/koitharu/kotatsu/core/parser/MangaParser.kt
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt
index 957b1c846..ee052cfdd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt
@@ -14,7 +14,6 @@ import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
-import java.net.HttpURLConnection
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@@ -27,6 +26,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
+import java.net.HttpURLConnection
private const val FALLBACK_SIZE = 9999 // largest icon
@@ -150,7 +150,7 @@ class FaviconFetcher(
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
return if (data.scheme == URI_SCHEME_FAVICON) {
- val mangaSource = MangaSource(data.schemeSpecificPart) ?: return null
+ val mangaSource = MangaSource(data.schemeSpecificPart)
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
} else {
null
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt
deleted file mode 100644
index 0efa45c92..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.koitharu.kotatsu.core.prefs
-
-enum class AppSection {
-
- LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
-}
\ 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 a9afbd6ec..cfbe5a082 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 org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
+import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue
@@ -44,14 +45,26 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val remoteMangaSources: Set
get() = Collections.unmodifiableSet(remoteSources)
+ var shelfSections: List
+ get() {
+ val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
+ val values = enumValues()
+ if (raw.isNullOrEmpty()) {
+ return values.toList()
+ }
+ return raw.split('|')
+ .mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
+ .distinct()
+ }
+ set(value) {
+ val raw = value.joinToString("|") { it.ordinal.toString() }
+ prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
+ }
+
var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
- var defaultSection: AppSection
- get() = prefs.getEnumValue(KEY_APP_SECTION, AppSection.HISTORY)
- set(value) = prefs.edit { putEnumValue(KEY_APP_SECTION, value) }
-
val theme: Int
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
@@ -342,6 +355,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
+ const val KEY_SHELF_SECTIONS = "shelf_sections_2"
// About
const val KEY_APP_UPDATE = "app_update"
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
index d34e753ab..4a3dd8ed5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
@@ -115,4 +115,4 @@ class ZipOutput(
closeEntry()
return true
}
-}
\ No newline at end of file
+}
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 1967bbb2a..931d09ae9 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
@@ -17,7 +17,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
@@ -36,6 +35,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
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.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
@@ -219,10 +219,8 @@ class DownloadManager @AssistedInject constructor(
val call = okHttp.newCall(request)
val file = File(destination, tempFileName)
val response = call.clone().await()
- runInterruptible(Dispatchers.IO) {
- file.outputStream().use { out ->
- checkNotNull(response.body).byteStream().copyTo(out)
- }
+ file.outputStream().use { out ->
+ checkNotNull(response.body).byteStream().copyToSuspending(out)
}
return file
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt
similarity index 72%
rename from app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
rename to app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt
index 3728c57b2..042d30bc3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt
@@ -6,24 +6,23 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.base.ui.AlertDialogFragment
-import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
+import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.DialogListModeBinding
import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
+import javax.inject.Inject
@AndroidEntryPoint
-class ListModeSelectDialog :
- AlertDialogFragment(),
- CheckableButtonGroup.OnCheckedChangeListener,
- Slider.OnChangeListener {
+class ListModeBottomSheet :
+ BaseBottomSheet(),
+ Slider.OnChangeListener,
+ MaterialButtonToggleGroup.OnButtonCheckedListener {
@Inject
lateinit var settings: AppSettings
@@ -33,13 +32,6 @@ class ListModeSelectDialog :
container: ViewGroup?,
) = DialogListModeBinding.inflate(inflater, container, false)
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
- return super.onBuildDialog(builder)
- .setTitle(R.string.list_mode)
- .setPositiveButton(R.string.done, null)
- .setCancelable(true)
- }
-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mode = settings.listMode
@@ -53,10 +45,10 @@ class ListModeSelectDialog :
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
binding.sliderGrid.addOnChangeListener(this)
- binding.checkableGroup.onCheckedChangeListener = this
+ binding.checkableGroup.addOnButtonCheckedListener(this)
}
- override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
+ override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
val mode = when (checkedId) {
R.id.button_list -> ListMode.LIST
R.id.button_list_detailed -> ListMode.DETAILED_LIST
@@ -78,6 +70,6 @@ class ListModeSelectDialog :
private const val TAG = "ListModeSelectDialog"
- fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
+ fun show(fm: FragmentManager) = ListModeBottomSheet().show(fm, TAG)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt
index 592ed9369..fcc52e1d2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt
@@ -17,9 +17,10 @@ class MangaListMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_list_mode -> {
- ListModeSelectDialog.show(fragment.childFragmentManager)
+ ListModeBottomSheet.show(fragment.childFragmentManager)
true
}
+
else -> false
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt
index 82bbead60..f8321b597 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt
@@ -3,15 +3,17 @@ package org.koitharu.kotatsu.local.data
import android.content.Context
import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.koitharu.kotatsu.utils.FileSize
+import org.koitharu.kotatsu.utils.ext.copyToSuspending
+import org.koitharu.kotatsu.utils.ext.longHashCode
+import org.koitharu.kotatsu.utils.ext.subdir
+import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
-import kotlinx.coroutines.flow.MutableStateFlow
-import org.koitharu.kotatsu.utils.FileSize
-import org.koitharu.kotatsu.utils.ext.longHashCode
-import org.koitharu.kotatsu.utils.ext.subdir
-import org.koitharu.kotatsu.utils.ext.takeIfReadable
@Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
@@ -26,42 +28,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
return lruCache.get(url)?.takeIfReadable()
}
- fun put(url: String, inputStream: InputStream): File {
+ suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
val file = File(cacheDir, url.longHashCode().toString())
- file.outputStream().use { out ->
- inputStream.copyTo(out)
- }
- val res = lruCache.put(url, file)
- file.delete()
- return res
- }
-
- fun put(
- url: String,
- inputStream: InputStream,
- contentLength: Long,
- progress: MutableStateFlow,
- ): File {
- val file = File(cacheDir, url.longHashCode().toString())
- file.outputStream().use { out ->
- var bytesCopied: Long = 0
- val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
- var bytes = inputStream.read(buffer)
- while (bytes >= 0) {
- out.write(buffer, 0, bytes)
- bytesCopied += bytes
- publishProgress(contentLength, bytesCopied, progress)
- bytes = inputStream.read(buffer)
+ try {
+ file.outputStream().use { out ->
+ inputStream.copyToSuspending(out)
}
- }
- val res = lruCache.put(url, file)
- file.delete()
- return res
- }
-
- private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow) {
- if (contentLength > 0) {
- progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
+ lruCache.put(url, file)
+ } finally {
+ file.delete()
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt
index 956d19676..d569af6c3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt
@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.documentfile.provider.DocumentFile
-import java.io.File
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -14,8 +13,10 @@ 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.model.RATING_UNKNOWN
+import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longOf
+import java.io.File
// TODO: Add support for chapters in cbz
// https://github.com/KotatsuApp/Kotatsu/issues/31
@@ -62,6 +63,7 @@ class DirMangaImporter(
file.isDirectory -> {
addPages(output, file, path + "/" + file.name, state)
}
+
file.isFile -> {
val tempFile = file.asTempFile()
if (!state.hasCover) {
@@ -86,7 +88,7 @@ class DirMangaImporter(
"Cannot open input stream for $uri"
}.use { input ->
file.outputStream().use { output ->
- input.copyTo(output)
+ input.copyToSuspending(output)
}
}
return file
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/ZipMangaImporter.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/ZipMangaImporter.kt
index a60d8e39e..fdf24abd1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/ZipMangaImporter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/ZipMangaImporter.kt
@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.local.domain.importer
import android.net.Uri
-import java.io.File
-import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
@@ -11,7 +9,10 @@ import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.resolveName
+import java.io.File
+import java.io.IOException
class ZipMangaImporter(
storageManager: LocalStorageManager,
@@ -27,10 +28,10 @@ class ZipMangaImporter(
}
val dest = File(getOutputDir(), name)
runInterruptible {
- contentResolver.openInputStream(uri)?.use { source ->
- dest.outputStream().use { output ->
- source.copyTo(output)
- }
+ contentResolver.openInputStream(uri)
+ }?.use { source ->
+ dest.outputStream().use { output ->
+ source.copyToSuspending(output)
}
} ?: throw IOException("Cannot open input stream: $uri")
localMangaRepository.getFromFile(dest)
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 03b0a90c5..72fbd1a0f 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
@@ -17,6 +17,7 @@ import androidx.core.graphics.Insets
import androidx.core.util.size
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
@@ -24,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.whenResumed
import androidx.transition.TransitionManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
@@ -129,6 +131,7 @@ class MainActivity :
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged)
viewModel.isFeedAvailable.observe(this, ::onFeedAvailabilityChanged)
+ searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -152,11 +155,7 @@ class MainActivity :
}
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
- if (fragment is ShelfFragment) {
- binding.fab?.show()
- } else {
- binding.fab?.hide()
- }
+ adjustFabVisibility(topFragment = fragment)
if (fromUser) {
binding.appbar.setExpanded(true)
}
@@ -278,6 +277,16 @@ class MainActivity :
navigationDelegate.setItemVisibility(R.id.nav_feed, isFeedAvailable)
}
+ private fun onIncognitoModeChanged(isIncognito: Boolean) {
+ var options = binding.searchView.imeOptions
+ options = if (isIncognito) {
+ options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
+ } else {
+ options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
+ }
+ binding.searchView.imeOptions = options
+ }
+
private fun onLoadingStateChanged(isLoading: Boolean) {
binding.fab?.isEnabled = !isLoading
}
@@ -313,8 +322,13 @@ class MainActivity :
private fun onFirstStart() {
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
when {
- !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
- settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
+ !settings.isSourcesSelected -> whenResumed {
+ OnboardDialogFragment.showWelcome(supportFragmentManager)
+ }
+
+ settings.newSources.isNotEmpty() -> whenResumed {
+ NewSourcesDialogFragment.show(supportFragmentManager)
+ }
}
withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext)
@@ -329,18 +343,18 @@ class MainActivity :
topFragment: Fragment? = navigationDelegate.primaryFragment,
isSearchOpened: Boolean = isSearchOpened(),
) {
- val fab = binding.fab
+ val fab = binding.fab ?: return
if (
isResumeEnabled &&
!actionModeDelegate.isActionModeStarted &&
!isSearchOpened &&
topFragment is ShelfFragment
) {
- if (fab?.isVisible == false) {
+ if (!fab.isVisible) {
fab.show()
}
} else {
- if (fab?.isVisible == true) {
+ if (fab.isVisible) {
fab.hide()
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt
index c94cdd153..4d09d621a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt
@@ -1,16 +1,16 @@
package org.koitharu.kotatsu.main.ui.protect
import android.app.Activity
-import android.app.Application
import android.content.Intent
import android.os.Bundle
+import org.acra.dialog.CrashReportDialog
+import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
+import org.koitharu.kotatsu.core.prefs.AppSettings
import javax.inject.Inject
import javax.inject.Singleton
-import org.acra.dialog.CrashReportDialog
-import org.koitharu.kotatsu.core.prefs.AppSettings
@Singleton
-class AppProtectHelper @Inject constructor(private val settings: AppSettings) : Application.ActivityLifecycleCallbacks {
+class AppProtectHelper @Inject constructor(private val settings: AppSettings) : DefaultActivityLifecycleCallbacks {
private var isUnlocked = settings.appPassword.isNullOrEmpty()
@@ -27,16 +27,6 @@ class AppProtectHelper @Inject constructor(private val settings: AppSettings) :
}
}
- override fun onActivityStarted(activity: Activity) = Unit
-
- override fun onActivityResumed(activity: Activity) = Unit
-
- override fun onActivityPaused(activity: Activity) = Unit
-
- override fun onActivityStopped(activity: Activity) = Unit
-
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
-
override fun onActivityDestroyed(activity: Activity) {
if (activity !is ProtectActivity && activity.isFinishing && activity.isTaskRoot) {
restoreLock()
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt
index 91332872b..0dd6af652 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt
@@ -7,14 +7,16 @@ import android.net.Uri
import androidx.collection.LongSparseArray
import androidx.collection.set
import dagger.hilt.android.qualifiers.ApplicationContext
-import java.io.File
-import java.util.*
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.zip.ZipFile
-import javax.inject.Inject
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
@@ -30,7 +32,16 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.connectivityManager
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+import org.koitharu.kotatsu.utils.ext.withProgress
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
+import java.io.File
+import java.util.LinkedList
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.zip.ZipFile
+import javax.inject.Inject
+import kotlin.coroutines.AbstractCoroutineContextElement
+import kotlin.coroutines.CoroutineContext
private const val PROGRESS_UNDEFINED = -1f
private const val PREFETCH_LIMIT_DEFAULT = 10
@@ -43,7 +54,7 @@ class PageLoader @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
) : Closeable {
- val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ val loaderScope = CoroutineScope(SupervisorJob() + InternalErrorHandler() + Dispatchers.Default)
private val connectivityManager = context.connectivityManager
private val tasks = LongSparseArray>()
@@ -56,7 +67,9 @@ class PageLoader @Inject constructor(
override fun close() {
loaderScope.cancel()
- tasks.clear()
+ synchronized(tasks) {
+ tasks.clear()
+ }
}
fun isPrefetchApplicable(): Boolean {
@@ -93,7 +106,9 @@ class PageLoader @Inject constructor(
return task
}
task = loadPageAsyncImpl(page)
- tasks[page.id] = task
+ synchronized(tasks) {
+ tasks[page.id] = task
+ }
return task
}
@@ -125,7 +140,9 @@ class PageLoader @Inject constructor(
while (prefetchQueue.isNotEmpty()) {
val page = prefetchQueue.pollFirst() ?: return
if (cache[page.url] == null) {
- tasks[page.id] = loadPageAsyncImpl(page)
+ synchronized(tasks) {
+ tasks[page.id] = loadPageAsyncImpl(page)
+ }
return
}
}
@@ -163,9 +180,12 @@ class PageLoader @Inject constructor(
val uri = Uri.parse(pageUrl)
return if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
- val zip = ZipFile(uri.schemeSpecificPart)
- val entry = zip.getEntry(uri.fragment)
- zip.getInputStream(entry).use {
+ ZipFile(uri.schemeSpecificPart)
+ }.use { zip ->
+ runInterruptible(Dispatchers.IO) {
+ val entry = zip.getEntry(uri.fragment)
+ zip.getInputStream(entry)
+ }.use {
cache.put(pageUrl, it)
}
}
@@ -184,10 +204,8 @@ class PageLoader @Inject constructor(
val body = checkNotNull(response.body) {
"Null response"
}
- runInterruptible(Dispatchers.IO) {
- body.byteStream().use {
- cache.put(pageUrl, it, body.contentLength(), progress)
- }
+ body.withProgress(progress).byteStream().use {
+ cache.put(pageUrl, it)
}
}
}
@@ -197,4 +215,13 @@ class PageLoader @Inject constructor(
val deferred = CompletableDeferred(file)
return ProgressDeferred(deferred, emptyProgressFlow)
}
+
+ private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
+ CoroutineExceptionHandler {
+
+ override fun handleException(context: CoroutineContext, exception: Throwable) {
+ exception.printStackTraceDebug()
+ }
+
+ }
}
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 17b95bf05..4fdeeab85 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
@@ -6,10 +6,6 @@ import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
-import java.io.File
-import javax.inject.Inject
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -20,6 +16,11 @@ import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader
+import org.koitharu.kotatsu.utils.ext.copyToSuspending
+import java.io.File
+import javax.inject.Inject
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
private const val MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png"
@@ -48,12 +49,12 @@ class PageSaveHelper @Inject constructor(
}
}
runInterruptible(Dispatchers.IO) {
- contentResolver.openOutputStream(destination)?.use { output ->
- pageFile.inputStream().use { input ->
- input.copyTo(output)
- }
- } ?: throw IOException("Output stream is null")
- }
+ contentResolver.openOutputStream(destination)
+ }?.use { output ->
+ pageFile.inputStream().use { input ->
+ input.copyToSuspending(output)
+ }
+ } ?: throw IOException("Output stream is null")
return destination
}
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 3510318dc..e4f97f1f5 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
@@ -58,6 +58,7 @@ import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
import org.koitharu.kotatsu.utils.ext.postDelayed
import org.koitharu.kotatsu.utils.ext.report
+import org.koitharu.kotatsu.utils.ext.setValueRounded
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -388,7 +389,7 @@ class ReaderActivity :
}
if (uiState.isSliderAvailable()) {
binding.slider.valueTo = uiState.totalPages.toFloat() - 1
- binding.slider.value = uiState.currentPage.toFloat()
+ binding.slider.setValueRounded(uiState.currentPage.toFloat())
binding.slider.isVisible = true
} else {
binding.slider.isVisible = false
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt
index 437e9b230..ee94a4f87 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt
@@ -103,8 +103,8 @@ class ColorFilterConfigActivity :
}
private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) {
- binding.sliderBrightness.value = readerColorFilter?.brightness ?: 0f
- binding.sliderContrast.value = readerColorFilter?.contrast ?: 0f
+ binding.sliderBrightness.setValueRounded(readerColorFilter?.brightness ?: 0f)
+ binding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f)
binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt
index 0eb155935..51b33d0b3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
+import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -65,7 +66,7 @@ class ReaderConfigBottomSheet :
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
findCallback()?.run {
- binding.sliderTimer.value = pageSwitchDelay
+ binding.sliderTimer.setValueRounded(pageSwitchDelay)
}
}
@@ -75,13 +76,16 @@ class ReaderConfigBottomSheet :
startActivity(SettingsActivity.newReaderSettingsIntent(v.context))
dismissAllowingStateLoss()
}
+
R.id.button_save_page -> {
val page = viewModel.getCurrentPage() ?: return
viewModel.saveCurrentPage(page, savePageRequest)
}
+
R.id.button_screen_rotate -> {
orientationHelper?.toggleOrientation()
}
+
R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return
val manga = viewModel.manga ?: return
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt
index e8c9fcc9e..03380ff51 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt
@@ -5,6 +5,7 @@ import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -13,11 +14,12 @@ abstract class BasePageHolder(
protected val binding: B,
loader: PageLoader,
settings: ReaderSettings,
+ networkStateObserver: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
- protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver)
+ protected val delegate = PageHolderDelegate(loader, settings, this, networkStateObserver, exceptionResolver)
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
val context: Context
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
index 33456c7d3..1a914b41e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
@@ -4,17 +4,19 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.utils.ext.resetTransformations
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
@Suppress("LeakingThis")
abstract class BaseReaderAdapter>(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
+ private val networkState: NetworkStateObserver,
private val exceptionResolver: ExceptionResolver,
) : RecyclerView.Adapter() {
@@ -56,9 +58,9 @@ abstract class BaseReaderAdapter>(
final override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
- ): H = onCreateViewHolder(parent, loader, readerSettings, exceptionResolver)
+ ): H = onCreateViewHolder(parent, loader, readerSettings, networkState, exceptionResolver)
- suspend fun setItems(items: List) = suspendCoroutine { cont ->
+ suspend fun setItems(items: List) = suspendCoroutine { cont ->
differ.submitList(items) {
cont.resume(Unit)
}
@@ -68,6 +70,7 @@ abstract class BaseReaderAdapter>(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
): H
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt
index 7bee7ca84..27431e75b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt
@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -26,6 +28,7 @@ class PageHolderDelegate(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
private val callback: Callback,
+ private val networkState: NetworkStateObserver,
private val exceptionResolver: ExceptionResolver,
) : DefaultOnImageEventListener, Observer {
@@ -118,29 +121,35 @@ class PageHolderDelegate(
}
}
- private suspend fun CoroutineScope.doLoad(data: MangaPage, force: Boolean) {
+ private suspend fun doLoad(data: MangaPage, force: Boolean) {
state = State.LOADING
error = null
callback.onLoadingStarted()
try {
val task = loader.loadPageAsync(data, force)
- val progressObserver = observeProgress(this, task.progressAsFlow())
- val file = task.await()
- progressObserver.cancel()
- this@PageHolderDelegate.file = file
+ file = coroutineScope {
+ val progressObserver = observeProgress(this, task.progressAsFlow())
+ val file = task.await()
+ progressObserver.cancel()
+ file
+ }
state = State.LOADED
- callback.onImageReady(file.toUri())
+ callback.onImageReady(checkNotNull(file).toUri())
} catch (e: CancellationException) {
throw e
- } catch (e: Exception) {
+ } catch (e: Throwable) {
state = State.ERROR
error = e
callback.onError(e)
+ if (e is IOException && !networkState.value) {
+ networkState.awaitForConnection()
+ retry(data)
+ }
}
}
private fun observeProgress(scope: CoroutineScope, progress: Flow) = progress
- .debounce(500)
+ .debounce(250)
.onEach { callback.onProgressChanged((100 * it).toInt()) }
.launchIn(scope)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
index f89930ec0..8e686ae81 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
@@ -6,6 +6,7 @@ import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -15,8 +16,9 @@ class ReversedPageHolder(
binding: ItemPageBinding,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
-) : PageHolder(binding, loader, settings, exceptionResolver) {
+) : PageHolder(binding, loader, settings, networkState, exceptionResolver) {
init {
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
@@ -35,6 +37,7 @@ class ReversedPageHolder(
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
resetScaleAndCenter()
}
+
ZoomMode.FIT_HEIGHT -> {
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = height / sHeight.toFloat()
@@ -43,6 +46,7 @@ class ReversedPageHolder(
PointF(sWidth.toFloat(), sHeight / 2f),
)
}
+
ZoomMode.FIT_WIDTH -> {
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = width / sWidth.toFloat()
@@ -51,6 +55,7 @@ class ReversedPageHolder(
PointF(sWidth / 2f, 0f),
)
}
+
ZoomMode.KEEP_START -> {
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
setScaleAndCenter(
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt
index 46c1f1690..d68f39334 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class ReversedPagesAdapter(
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
-) : BaseReaderAdapter(loader, settings, exceptionResolver) {
+) : BaseReaderAdapter(loader, settings, networkState, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) = ReversedPageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
settings = settings,
+ networkState = networkState,
exceptionResolver = exceptionResolver,
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt
index 9a1c297f2..df4a739ac 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt
@@ -7,8 +7,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
-import kotlin.math.absoluteValue
import kotlinx.coroutines.async
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -19,10 +19,15 @@ import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.recyclerView
import org.koitharu.kotatsu.utils.ext.resetTransformations
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
+import javax.inject.Inject
+import kotlin.math.absoluteValue
@AndroidEntryPoint
class ReversedReaderFragment : BaseReader() {
+ @Inject
+ lateinit var networkStateObserver: NetworkStateObserver
+
private var pagerAdapter: ReversedPagesAdapter? = null
override fun onInflateView(
@@ -33,7 +38,12 @@ class ReversedReaderFragment : BaseReader() {
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
+ pagerAdapter = ReversedPagesAdapter(
+ viewModel.pageLoader,
+ viewModel.readerSettings,
+ networkStateObserver,
+ exceptionResolver,
+ )
with(binding.pager) {
adapter = pagerAdapter
offscreenPageLimit = 2
@@ -44,8 +54,8 @@ class ReversedReaderFragment : BaseReader() {
val transformer = if (it) ReversedPageAnimTransformer() else null
binding.pager.setPageTransformer(transformer)
if (transformer == null) {
- binding.pager.recyclerView?.children?.forEach {
- it.resetTransformations()
+ binding.pager.recyclerView?.children?.forEach { v ->
+ v.resetTransformations()
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
index 713bfe306..b56163f54 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
@@ -10,6 +10,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -21,8 +22,9 @@ open class PageHolder(
binding: ItemPageBinding,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
-) : BasePageHolder(binding, loader, settings, exceptionResolver),
+) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener {
init {
@@ -74,6 +76,7 @@ open class PageHolder(
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.ssiv.resetScaleAndCenter()
}
+
ZoomMode.FIT_HEIGHT -> {
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
@@ -82,6 +85,7 @@ open class PageHolder(
PointF(0f, binding.ssiv.sHeight / 2f),
)
}
+
ZoomMode.FIT_WIDTH -> {
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
@@ -90,6 +94,7 @@ open class PageHolder(
PointF(binding.ssiv.sWidth / 2f, 0f),
)
}
+
ZoomMode.KEEP_START -> {
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.ssiv.setScaleAndCenter(
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
index a7d526d95..889f5189f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
@@ -7,8 +7,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
-import kotlin.math.absoluteValue
import kotlinx.coroutines.async
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -18,10 +18,15 @@ import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.recyclerView
import org.koitharu.kotatsu.utils.ext.resetTransformations
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
+import javax.inject.Inject
+import kotlin.math.absoluteValue
@AndroidEntryPoint
class PagerReaderFragment : BaseReader() {
+ @Inject
+ lateinit var networkStateObserver: NetworkStateObserver
+
private var pagesAdapter: PagesAdapter? = null
override fun onInflateView(
@@ -32,7 +37,12 @@ class PagerReaderFragment : BaseReader() {
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- pagesAdapter = PagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
+ pagesAdapter = PagesAdapter(
+ viewModel.pageLoader,
+ viewModel.readerSettings,
+ networkStateObserver,
+ exceptionResolver,
+ )
with(binding.pager) {
adapter = pagesAdapter
offscreenPageLimit = 2
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt
index 57badce92..293ca6273 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter(
loader: PageLoader,
settings: ReaderSettings,
+ networkStateObserver: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
-) : BaseReaderAdapter(loader, settings, exceptionResolver) {
+) : BaseReaderAdapter(loader, settings, networkStateObserver, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) = PageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
settings = settings,
+ networkState = networkState,
exceptionResolver = exceptionResolver,
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt
index ea76b62f8..6d92ff321 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -12,13 +12,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter(
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
-) : BaseReaderAdapter(loader, settings, exceptionResolver) {
+) : BaseReaderAdapter(loader, settings, networkState, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) = WebtoonHolder(
binding = ItemPageWebtoonBinding.inflate(
@@ -28,6 +30,7 @@ class WebtoonAdapter(
),
loader = loader,
settings = settings,
+ networkState = networkState,
exceptionResolver = exceptionResolver,
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt
index 264f21fe7..086081097 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt
@@ -8,20 +8,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
-import org.koitharu.kotatsu.utils.ext.*
+import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.hideCompat
+import org.koitharu.kotatsu.utils.ext.ifZero
+import org.koitharu.kotatsu.utils.ext.setProgressCompat
+import org.koitharu.kotatsu.utils.ext.showCompat
class WebtoonHolder(
binding: ItemPageWebtoonBinding,
loader: PageLoader,
settings: ReaderSettings,
+ networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
-) : BasePageHolder(binding, loader, settings, exceptionResolver),
+) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener {
private var scrollToRestore = 0
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
index 79a4e8927..7fe5fb1f4 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
+import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -15,10 +16,14 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
+import javax.inject.Inject
@AndroidEntryPoint
class WebtoonReaderFragment : BaseReader() {
+ @Inject
+ lateinit var networkStateObserver: NetworkStateObserver
+
private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null
@@ -29,7 +34,12 @@ class WebtoonReaderFragment : BaseReader() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
+ webtoonAdapter = WebtoonAdapter(
+ viewModel.pageLoader,
+ viewModel.readerSettings,
+ networkStateObserver,
+ exceptionResolver,
+ )
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = webtoonAdapter
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
index eb7242065..6d14265be 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
@@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
+import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
@@ -15,7 +16,6 @@ import org.koitharu.kotatsu.databinding.ActivitySearchBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.utils.ext.showKeyboard
-import kotlin.text.Typography.dagger
@AndroidEntryPoint
class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
@@ -32,6 +32,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
}
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
with(binding.searchView) {
queryHint = getString(R.string.search_on_s, source.title)
setOnQueryTextListener(this@SearchActivity)
@@ -72,6 +73,16 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
override fun onQueryTextChange(newText: String?): Boolean = false
+ private fun onIncognitoModeChanged(isIncognito: Boolean) {
+ var options = binding.searchView.imeOptions
+ options = if (isIncognito) {
+ options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
+ } else {
+ options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
+ }
+ binding.searchView.imeOptions = options
+ }
+
companion object {
private const val EXTRA_SOURCE = "source"
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
index 377b2c17c..31edd3481 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
@@ -3,17 +3,28 @@ package org.koitharu.kotatsu.search.ui.suggestion
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.plus
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
+import javax.inject.Inject
private const val DEBOUNCE_TIMEOUT = 500L
private const val MAX_MANGA_ITEMS = 6
@@ -30,6 +41,12 @@ class SearchSuggestionViewModel @Inject constructor(
private val query = MutableStateFlow("")
private var suggestionJob: Job? = null
+ val isIncognitoModeEnabled = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_INCOGNITO_MODE,
+ valueProducer = { isIncognitoModeEnabled },
+ )
+
val suggestion = MutableLiveData>()
init {
@@ -41,7 +58,11 @@ class SearchSuggestionViewModel @Inject constructor(
}
fun saveQuery(query: String) {
- repository.saveSearchQuery(query)
+ launchJob(Dispatchers.Default) {
+ if (!settings.isIncognitoModeEnabled) {
+ repository.saveSearchQuery(query)
+ }
+ }
}
fun clearSearchHistory() {
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 1672657fd..82a7400b0 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
@@ -30,7 +30,7 @@ class OnboardViewModel @Inject constructor(
init {
if (settings.isSourcesSelected) {
- selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x)?.locale })
+ selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x).locale })
} else {
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
x.language
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt
new file mode 100644
index 000000000..c8089e431
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt
@@ -0,0 +1,35 @@
+package org.koitharu.kotatsu.shelf.domain
+
+import org.koitharu.kotatsu.core.model.FavouriteCategory
+import org.koitharu.kotatsu.history.domain.MangaWithHistory
+import org.koitharu.kotatsu.parsers.model.Manga
+
+class ShelfContent(
+ val history: List,
+ val favourites: Map>,
+ val updated: Map,
+ val local: List,
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ShelfContent
+
+ if (history != other.history) return false
+ if (favourites != other.favourites) return false
+ if (updated != other.updated) return false
+ if (local != other.local) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = history.hashCode()
+ result = 31 * result + favourites.hashCode()
+ result = 31 * result + updated.hashCode()
+ result = 31 * result + local.hashCode()
+ return result
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt
index c56a04a35..7ecfa7dbf 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt
@@ -21,15 +21,26 @@ 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.SortOrder
+import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
class ShelfRepository @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
+ private val trackingRepository: TrackingRepository,
private val db: MangaDatabase,
) {
+ fun observeShelfContent(): Flow = combine(
+ historyRepository.observeAllWithHistory(),
+ observeLocalManga(SortOrder.UPDATED),
+ observeFavourites(),
+ trackingRepository.observeUpdatedManga(),
+ ) { history, local, favorites, updated ->
+ ShelfContent(history, favorites, updated, local)
+ }
+
fun observeLocalManga(sortOrder: SortOrder): Flow> {
return flow {
emit(null)
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt
new file mode 100644
index 000000000..de24a9583
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt
@@ -0,0 +1,6 @@
+package org.koitharu.kotatsu.shelf.domain
+
+enum class ShelfSection {
+
+ HISTORY, LOCAL, UPDATED, FAVORITES;
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt
index 589432ed7..d00601406 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt
@@ -6,16 +6,16 @@ import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentManager
-import com.google.android.material.R as materialR
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import java.util.*
-import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
-import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfCategoriesConfigSheet
-import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
+import org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity
+import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
import org.koitharu.kotatsu.utils.ext.startOfDay
+import java.util.Date
+import java.util.concurrent.TimeUnit
+import com.google.android.material.R as materialR
class ShelfMenuProvider(
private val context: Context,
@@ -33,18 +33,22 @@ class ShelfMenuProvider(
showClearHistoryDialog()
true
}
+
R.id.action_grid_size -> {
ShelfSizeBottomSheet.show(fragmentManager)
true
}
+
R.id.action_import -> {
ImportDialogFragment.show(fragmentManager)
true
}
+
R.id.action_categories -> {
- ShelfCategoriesConfigSheet.show(fragmentManager)
+ context.startActivity(ShelfSettingsActivity.newIntent(context))
true
}
+
else -> false
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
index 2e88331b2..a56d848ea 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.os.NetworkStateObserver
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.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
@@ -29,8 +30,9 @@ import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.koitharu.kotatsu.parsers.model.SortOrder
+import org.koitharu.kotatsu.shelf.domain.ShelfContent
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
+import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
@@ -44,19 +46,17 @@ class ShelfViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
- private val networkStateObserver: NetworkStateObserver,
+ networkStateObserver: NetworkStateObserver,
) : BaseViewModel(), ListExtraProvider {
val onActionDone = SingleLiveEvent()
val content: LiveData> = combine(
+ settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
networkStateObserver,
- historyRepository.observeAllWithHistory(),
- repository.observeLocalManga(SortOrder.UPDATED),
- repository.observeFavourites(),
- trackingRepository.observeUpdatedManga(),
- ) { isConnected, history, local, favourites, updated ->
- mapList(history, favourites, updated, local, isConnected)
+ repository.observeShelfContent(),
+ ) { sections, isConnected, content ->
+ mapList(content, sections, isConnected)
}.debounce(500)
.catch { e ->
emit(listOf(e.toErrorState(canRetry = false)))
@@ -134,25 +134,19 @@ class ShelfViewModel @Inject constructor(
}
private suspend fun mapList(
- history: List,
- favourites: Map>,
- updated: Map,
- local: List,
+ content: ShelfContent,
+ sections: List,
isNetworkAvailable: Boolean,
): List {
- val result = ArrayList(favourites.keys.size + 3)
+ val result = ArrayList(content.favourites.keys.size + 3)
if (isNetworkAvailable) {
- if (history.isNotEmpty()) {
- mapHistory(result, history)
- }
- if (local.isNotEmpty()) {
- mapLocal(result, local)
- }
- if (updated.isNotEmpty()) {
- mapUpdated(result, updated)
- }
- if (favourites.isNotEmpty()) {
- mapFavourites(result, favourites)
+ for (section in sections) {
+ when (section) {
+ ShelfSection.HISTORY -> mapHistory(result, content.history)
+ ShelfSection.LOCAL -> mapLocal(result, content.local)
+ ShelfSection.UPDATED -> mapUpdated(result, content.updated)
+ ShelfSection.FAVORITES -> mapFavourites(result, content.favourites)
+ }
}
} else {
result += EmptyHint(
@@ -161,12 +155,17 @@ class ShelfViewModel @Inject constructor(
textSecondary = R.string.network_unavailable_hint,
actionStringRes = R.string.manage,
)
- val offlineHistory = history.filter { it.manga.source == MangaSource.LOCAL }
- if (offlineHistory.isNotEmpty()) {
- mapHistory(result, offlineHistory)
- }
- if (local.isNotEmpty()) {
- mapLocal(result, local)
+ for (section in sections) {
+ when (section) {
+ ShelfSection.HISTORY -> mapHistory(
+ result,
+ content.history.filter { it.manga.source == MangaSource.LOCAL },
+ )
+
+ ShelfSection.LOCAL -> mapLocal(result, content.local)
+ ShelfSection.UPDATED -> Unit
+ ShelfSection.FAVORITES -> Unit
+ }
}
}
if (result.isEmpty()) {
@@ -189,6 +188,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList,
list: List,
) {
+ if (list.isEmpty()) {
+ return
+ }
val showPercent = settings.isReadingIndicatorsEnabled
destination += ShelfSectionModel.History(
items = list.map { (manga, history) ->
@@ -204,6 +206,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList,
updated: Map,
) {
+ if (updated.isEmpty()) {
+ return
+ }
val showPercent = settings.isReadingIndicatorsEnabled
destination += ShelfSectionModel.Updated(
items = updated.map { (manga, counter) ->
@@ -218,6 +223,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList,
local: List,
) {
+ if (local.isEmpty()) {
+ return
+ }
destination += ShelfSectionModel.Local(
items = local.toUi(ListMode.GRID, this),
showAllButtonText = R.string.show_all,
@@ -228,6 +236,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList,
favourites: Map>,
) {
+ if (favourites.isEmpty()) {
+ return
+ }
for ((category, list) in favourites) {
if (list.isNotEmpty()) {
destination += ShelfSectionModel.Favourites(
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt
new file mode 100644
index 000000000..ef28b06a5
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt
@@ -0,0 +1,101 @@
+package org.koitharu.kotatsu.shelf.ui.config
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.viewModels
+import androidx.core.graphics.Insets
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.base.ui.BaseActivity
+import org.koitharu.kotatsu.databinding.ActivityShelfSettingsBinding
+
+@AndroidEntryPoint
+class ShelfSettingsActivity :
+ BaseActivity(),
+ View.OnClickListener, ShelfSettingsListener {
+
+ private val viewModel by viewModels()
+ private lateinit var reorderHelper: ItemTouchHelper
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityShelfSettingsBinding.inflate(layoutInflater))
+ supportActionBar?.run {
+ setDisplayHomeAsUpEnabled(true)
+ setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material)
+ }
+ binding.buttonDone.setOnClickListener(this)
+ val settingsAdapter = ShelfSettingsAdapter(this)
+ with(binding.recyclerView) {
+ setHasFixedSize(true)
+ adapter = settingsAdapter
+ reorderHelper = ItemTouchHelper(SectionsReorderCallback()).also {
+ it.attachToRecyclerView(this)
+ }
+ }
+
+
+ viewModel.content.observe(this) { settingsAdapter.items = it }
+ }
+
+ override fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean) {
+ viewModel.setItemChecked(item, isChecked)
+ }
+
+ override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
+ reorderHelper.startDrag(holder)
+ }
+
+ override fun onClick(v: View?) {
+ finishAfterTransition()
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ binding.root.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ )
+ binding.recyclerView.updatePadding(
+ bottom = insets.bottom,
+ )
+ binding.toolbar.updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
+
+ private inner class SectionsReorderCallback : ItemTouchHelper.SimpleCallback(
+ ItemTouchHelper.DOWN or ItemTouchHelper.UP,
+ 0,
+ ) {
+
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder,
+ ): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSections(
+ viewHolder.bindingAdapterPosition,
+ target.bindingAdapterPosition,
+ )
+
+ override fun canDropOver(
+ recyclerView: RecyclerView,
+ current: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder,
+ ): Boolean = current.itemViewType == target.itemViewType
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
+
+ override fun isLongPressDragEnabled() = false
+ }
+
+ companion object {
+
+ fun newIntent(context: Context) = Intent(context, ShelfSettingsActivity::class.java)
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt
new file mode 100644
index 000000000..19aaa81cf
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt
@@ -0,0 +1,41 @@
+package org.koitharu.kotatsu.shelf.ui.config
+
+import androidx.recyclerview.widget.DiffUtil
+import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
+
+class ShelfSettingsAdapter(
+ listener: ShelfSettingsListener,
+) : AsyncListDifferDelegationAdapter(DiffCallback()) {
+
+ init {
+ delegatesManager.addDelegate(shelfCategoryAD(listener))
+ .addDelegate(shelfSectionAD(listener))
+ }
+
+ class DiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
+ return when {
+ oldItem is ShelfSettingsItemModel.Section && newItem is ShelfSettingsItemModel.Section -> {
+ oldItem.section == newItem.section
+ }
+
+ oldItem is ShelfSettingsItemModel.FavouriteCategory && newItem is ShelfSettingsItemModel.FavouriteCategory -> {
+ oldItem.id == newItem.id
+ }
+
+ else -> false
+ }
+ }
+
+ override fun areContentsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun getChangePayload(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Any? {
+ return if (oldItem.isChecked == newItem.isChecked) {
+ super.getChangePayload(oldItem, newItem)
+ } else Unit
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt
new file mode 100644
index 000000000..973391190
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt
@@ -0,0 +1,75 @@
+package org.koitharu.kotatsu.shelf.ui.config
+
+import android.annotation.SuppressLint
+import android.view.MotionEvent
+import android.view.View
+import android.widget.CompoundButton
+import androidx.core.view.updatePaddingRelative
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
+import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding
+import org.koitharu.kotatsu.shelf.domain.ShelfSection
+
+@SuppressLint("ClickableViewAccessibility")
+fun shelfSectionAD(
+ listener: ShelfSettingsListener,
+) =
+ adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemShelfSectionDraggableBinding.inflate(layoutInflater, parent, false) },
+ ) {
+
+ val eventListener = object :
+ View.OnTouchListener,
+ CompoundButton.OnCheckedChangeListener {
+
+ override fun onTouch(v: View?, event: MotionEvent): Boolean {
+ return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+ listener.onDragHandleTouch(this@adapterDelegateViewBinding)
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
+ listener.onItemCheckedChanged(item, isChecked)
+ }
+ }
+
+ binding.switchToggle.setOnCheckedChangeListener(eventListener)
+ binding.imageViewHandle.setOnTouchListener(eventListener)
+
+ bind {
+ binding.textViewTitle.setText(item.section.titleResId)
+ binding.switchToggle.isChecked = item.isChecked
+ }
+ }
+
+fun shelfCategoryAD(
+ listener: ShelfSettingsListener,
+) =
+ adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
+ ) {
+ itemView.setOnClickListener {
+ listener.onItemCheckedChanged(item, !item.isChecked)
+ }
+ binding.root.updatePaddingRelative(
+ start = binding.root.paddingStart * 2,
+ end = binding.root.paddingStart,
+ )
+
+ bind {
+ binding.root.text = item.title
+ binding.root.isChecked = item.isChecked
+ }
+ }
+
+private val ShelfSection.titleResId: Int
+ get() = when (this) {
+ ShelfSection.HISTORY -> R.string.history
+ ShelfSection.LOCAL -> R.string.local_storage
+ ShelfSection.UPDATED -> R.string.updated
+ ShelfSection.FAVORITES -> R.string.favourites
+ }
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt
new file mode 100644
index 000000000..e75f329de
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt
@@ -0,0 +1,60 @@
+package org.koitharu.kotatsu.shelf.ui.config
+
+import org.koitharu.kotatsu.list.ui.model.ListModel
+import org.koitharu.kotatsu.shelf.domain.ShelfSection
+
+sealed interface ShelfSettingsItemModel : ListModel {
+
+ val isChecked: Boolean
+
+ class Section(
+ val section: ShelfSection,
+ override val isChecked: Boolean,
+ ) : ShelfSettingsItemModel {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Section
+
+ if (section != other.section) return false
+ if (isChecked != other.isChecked) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = section.hashCode()
+ result = 31 * result + isChecked.hashCode()
+ return result
+ }
+ }
+
+ class FavouriteCategory(
+ val id: Long,
+ val title: String,
+ override val isChecked: Boolean,
+ ) : ShelfSettingsItemModel {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as FavouriteCategory
+
+ if (id != other.id) return false
+ if (title != other.title) return false
+ if (isChecked != other.isChecked) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = id.hashCode()
+ result = 31 * result + title.hashCode()
+ result = 31 * result + isChecked.hashCode()
+ return result
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt
new file mode 100644
index 000000000..b8fa749a1
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt
@@ -0,0 +1,10 @@
+package org.koitharu.kotatsu.shelf.ui.config
+
+import androidx.recyclerview.widget.RecyclerView
+
+interface ShelfSettingsListener {
+
+ fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean)
+
+ fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt
new file mode 100644
index 000000000..15323d9b8
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt
@@ -0,0 +1,101 @@
+package org.koitharu.kotatsu.shelf.ui.config
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+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.shelf.domain.ShelfSection
+import org.koitharu.kotatsu.utils.asFlowLiveData
+import org.koitharu.kotatsu.utils.ext.move
+import javax.inject.Inject
+
+@HiltViewModel
+class ShelfSettingsViewModel @Inject constructor(
+ private val favouritesRepository: FavouritesRepository,
+ private val settings: AppSettings,
+) : BaseViewModel() {
+
+ val content = combine(
+ settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
+ favouritesRepository.observeCategories(),
+ ) { sections, categories ->
+ buildList(sections, categories)
+ }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
+
+ private var updateJob: Job? = null
+
+ fun setItemChecked(item: ShelfSettingsItemModel, isChecked: Boolean) {
+ val prevJob = updateJob
+ updateJob = launchJob(Dispatchers.Default) {
+ prevJob?.join()
+ when (item) {
+ is ShelfSettingsItemModel.FavouriteCategory -> {
+ favouritesRepository.updateCategory(item.id, isChecked)
+ }
+
+ is ShelfSettingsItemModel.Section -> {
+ val sections = settings.shelfSections
+ settings.shelfSections = if (isChecked) {
+ sections + item.section
+ } else {
+ if (sections.size > 1) {
+ sections - item.section
+ } else {
+ return@launchJob
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun reorderSections(oldPos: Int, newPos: Int): Boolean {
+ val snapshot = content.value?.toMutableList() ?: return false
+ snapshot.move(oldPos, newPos)
+ settings.shelfSections = snapshot.sections()
+ return true
+ }
+
+ private fun buildList(
+ sections: List,
+ categories: List
+ ): List {
+ val result = ArrayList()
+ val sectionsList = ShelfSection.values().toMutableList()
+ for (section in sections) {
+ sectionsList.remove(section)
+ result.addSection(section, true, categories)
+ }
+ for (section in sectionsList) {
+ result.addSection(section, false, categories)
+ }
+ return result
+ }
+
+ private fun MutableList.addSection(
+ section: ShelfSection,
+ isEnabled: Boolean,
+ favouriteCategories: List,
+ ) {
+ add(ShelfSettingsItemModel.Section(section, isEnabled))
+ if (isEnabled && section == ShelfSection.FAVORITES) {
+ favouriteCategories.mapTo(this) {
+ ShelfSettingsItemModel.FavouriteCategory(
+ id = it.id,
+ title = it.title,
+ isChecked = it.isVisibleInLibrary,
+ )
+ }
+ }
+ }
+
+ private fun List.sections(): List {
+ return mapNotNull { (it as? ShelfSettingsItemModel.Section)?.takeIf { x -> x.isChecked }?.section }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt
deleted file mode 100644
index 12d0b4a5d..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.koitharu.kotatsu.shelf.ui.config.categories
-
-import androidx.recyclerview.widget.DiffUtil
-import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
-import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
-import org.koitharu.kotatsu.core.model.FavouriteCategory
-
-class ShelfCategoriesConfigAdapter(
- listener: OnListItemClickListener,
-) : AsyncListDifferDelegationAdapter(DiffCallback()) {
-
- init {
- delegatesManager.addDelegate(shelfCategoryAD(listener))
- }
-
- class DiffCallback : DiffUtil.ItemCallback() {
-
- override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
- return oldItem.id == newItem.id
- }
-
- override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
- return oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary && oldItem.title == newItem.title
- }
-
- override fun getChangePayload(oldItem: FavouriteCategory, newItem: FavouriteCategory): Any? {
- return if (oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary) {
- super.getChangePayload(oldItem, newItem)
- } else Unit
- }
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigSheet.kt
deleted file mode 100644
index 5df721191..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigSheet.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.koitharu.kotatsu.shelf.ui.config.categories
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.fragment.app.FragmentManager
-import androidx.fragment.app.viewModels
-import dagger.hilt.android.AndroidEntryPoint
-import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.base.ui.BaseBottomSheet
-import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
-import org.koitharu.kotatsu.core.model.FavouriteCategory
-import org.koitharu.kotatsu.databinding.SheetBaseBinding
-
-@AndroidEntryPoint
-class ShelfCategoriesConfigSheet :
- BaseBottomSheet(),
- OnListItemClickListener,
- View.OnClickListener {
-
- private val viewModel by viewModels()
-
- override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
- return SheetBaseBinding.inflate(inflater, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.headerBar.toolbar.setTitle(R.string.favourites_categories)
- binding.buttonDone.isVisible = true
- binding.buttonDone.setOnClickListener(this)
- val adapter = ShelfCategoriesConfigAdapter(this)
- binding.recyclerView.adapter = adapter
-
- viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
- }
-
- override fun onItemClick(item: FavouriteCategory, view: View) {
- viewModel.toggleItem(item)
- }
-
- override fun onClick(v: View?) {
- dismiss()
- }
-
- companion object {
-
- private const val TAG = "ShelfCategoriesConfigSheet"
-
- fun show(fm: FragmentManager) = ShelfCategoriesConfigSheet().show(fm, TAG)
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt
deleted file mode 100644
index 1c77b243e..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.koitharu.kotatsu.shelf.ui.config.categories
-
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import org.koitharu.kotatsu.base.ui.BaseViewModel
-import org.koitharu.kotatsu.core.model.FavouriteCategory
-import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
-import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
-
-@HiltViewModel
-class ShelfCategoriesConfigViewModel @Inject constructor(
- private val favouritesRepository: FavouritesRepository,
-) : BaseViewModel() {
-
- val content = favouritesRepository.observeCategories()
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
-
- private var updateJob: Job? = null
-
- fun toggleItem(category: FavouriteCategory) {
- val prevJob = updateJob
- updateJob = launchJob(Dispatchers.Default) {
- prevJob?.join()
- favouritesRepository.updateCategory(category.id, !category.isVisibleInLibrary)
- }
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt
deleted file mode 100644
index 9aa529210..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.koitharu.kotatsu.shelf.ui.config.categories
-
-import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
-import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
-import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
-import org.koitharu.kotatsu.core.model.FavouriteCategory
-import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
-
-fun shelfCategoryAD(
- listener: OnListItemClickListener,
-) = adapterDelegateViewBinding(
- { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
-) {
- val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
- itemView.setOnClickListener(eventListener)
-
- bind {
- binding.root.text = item.title
- binding.root.isChecked = item.isVisibleInLibrary
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt
index dd84a891b..9677f8d4e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt
@@ -8,13 +8,13 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.SheetShelfSizeBinding
import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
+import javax.inject.Inject
@AndroidEntryPoint
class ShelfSizeBottomSheet :
@@ -51,9 +51,10 @@ class ShelfSizeBottomSheet :
}
override fun onClick(v: View) {
+ val slider = binding.sliderGrid
when (v.id) {
- R.id.button_small -> binding.sliderGrid.value -= binding.sliderGrid.stepSize
- R.id.button_large -> binding.sliderGrid.value += binding.sliderGrid.stepSize
+ R.id.button_small -> slider.setValueRounded(slider.value - slider.stepSize)
+ R.id.button_large -> slider.setValueRounded(slider.value + slider.stepSize)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt
index d248de40e..d00ce5fc5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt
@@ -43,6 +43,9 @@ abstract class TracksDao {
@Query("DELETE FROM tracks")
abstract suspend fun clear()
+ @Query("UPDATE tracks SET chapters_new = 0")
+ abstract suspend fun clearCounters()
+
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: TrackEntity): Long
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 a9aac2ce1..c1038f83e 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
@@ -98,6 +98,8 @@ class TrackingRepository @Inject constructor(
suspend fun clearLogs() = db.trackLogsDao.clear()
+ suspend fun clearCounters() = db.tracksDao.clearCounters()
+
suspend fun gc() {
db.withTransaction {
db.tracksDao.gc()
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt
index e20d67933..e25f83428 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt
@@ -6,9 +6,9 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.MenuProvider
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.tracker.work.TrackWorker
@@ -39,13 +39,15 @@ class FeedMenuProvider(
}
R.id.action_clear_feed -> {
- MaterialAlertDialogBuilder(context)
+ CheckBoxAlertDialog.Builder(context)
.setTitle(R.string.clear_updates_feed)
.setMessage(R.string.text_clear_updates_feed_prompt)
.setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.clear) { _, _ ->
- viewModel.clearFeed()
- }.show()
+ .setCheckBoxChecked(true)
+ .setCheckBoxText(R.string.clear_new_chapters_counters)
+ .setPositiveButton(R.string.clear) { _, isChecked ->
+ viewModel.clearFeed(isChecked)
+ }.create().show()
true
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt
index 1401873e8..12dbdf497 100644
--- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt
@@ -50,9 +50,12 @@ class FeedViewModel @Inject constructor(
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
- fun clearFeed() {
+ fun clearFeed(clearCounters: Boolean) {
launchLoadingJob(Dispatchers.Default) {
repository.clearLogs()
+ if (clearCounters) {
+ repository.clearCounters()
+ }
onFeedCleared.postCall(Unit)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/IncognitoModeIndicator.kt b/app/src/main/java/org/koitharu/kotatsu/utils/IncognitoModeIndicator.kt
new file mode 100644
index 000000000..8ca72f3eb
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/IncognitoModeIndicator.kt
@@ -0,0 +1,46 @@
+package org.koitharu.kotatsu.utils
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.utils.ext.getThemeColor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class IncognitoModeIndicator @Inject constructor(
+ private val settings: AppSettings,
+) : DefaultActivityLifecycleCallbacks {
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ if (activity !is AppCompatActivity) {
+ return
+ }
+ settings.observeAsFlow(
+ key = AppSettings.KEY_INCOGNITO_MODE,
+ valueProducer = { isIncognitoModeEnabled },
+ ).flowOn(Dispatchers.IO)
+ .flowWithLifecycle(activity.lifecycle)
+ .onEach { updateStatusBar(activity, it) }
+ .launchIn(activity.lifecycleScope)
+ }
+
+ private fun updateStatusBar(activity: AppCompatActivity, isIncognitoModeEnabled: Boolean) {
+ activity.window.statusBarColor = if (isIncognitoModeEnabled) {
+ ContextCompat.getColor(activity, R.color.status_bar_incognito)
+ } else {
+ activity.getThemeColor(android.R.attr.statusBarColor)
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt
new file mode 100644
index 000000000..eb0e0de12
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt
@@ -0,0 +1,34 @@
+package org.koitharu.kotatsu.utils.ext
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.withContext
+import okhttp3.ResponseBody
+import org.koitharu.kotatsu.utils.progress.ProgressResponseBody
+import java.io.InputStream
+import java.io.OutputStream
+
+suspend fun InputStream.copyToSuspending(
+ out: OutputStream,
+ bufferSize: Int = DEFAULT_BUFFER_SIZE
+): Long = withContext(Dispatchers.IO) {
+ val job = currentCoroutineContext()[Job]
+ var bytesCopied: Long = 0
+ val buffer = ByteArray(bufferSize)
+ var bytes = read(buffer)
+ while (bytes >= 0) {
+ out.write(buffer, 0, bytes)
+ bytesCopied += bytes
+ job?.ensureActive()
+ bytes = read(buffer)
+ job?.ensureActive()
+ }
+ bytesCopied
+}
+
+fun ResponseBody.withProgress(progressState: MutableStateFlow): ResponseBody {
+ return ProgressResponseBody(this, progressState)
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt
index 9057a6cc4..e1f235652 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt
@@ -8,6 +8,7 @@ import okio.FileNotFoundException
import okio.IOException
import org.acra.ktx.sendWithAcra
import org.json.JSONException
+import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
@@ -45,6 +46,12 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404)
+
+ is HttpStatusException -> when (statusCode) {
+ in 500..599 -> resources.getString(R.string.server_error, statusCode)
+ else -> localizedMessage
+ }
+
is IOException -> getDisplayMessage(message, resources) ?: localizedMessage
else -> localizedMessage
} ?: resources.getString(R.string.error_occurred)
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
index 29d0953c7..5d00f4a11 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
@@ -127,7 +127,11 @@ fun RecyclerView.ViewHolder.getItem(clazz: Class): T? {
fun Slider.setValueRounded(newValue: Float) {
val step = stepSize
- val roundedValue = (newValue / step).roundToInt() * step
+ val roundedValue = if (step <= 0f) {
+ newValue
+ } else {
+ (newValue / step).roundToInt() * step
+ }
value = roundedValue.coerceIn(valueFrom, valueTo)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt
new file mode 100644
index 000000000..20327a272
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt
@@ -0,0 +1,51 @@
+package org.koitharu.kotatsu.utils.progress
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import okio.Buffer
+import okio.BufferedSource
+import okio.ForwardingSource
+import okio.Source
+import okio.buffer
+
+class ProgressResponseBody(
+ private val delegate: ResponseBody,
+ private val progressState: MutableStateFlow,
+) : ResponseBody() {
+
+ private var bufferedSource: BufferedSource? = null
+
+ override fun close() {
+ super.close()
+ delegate.close()
+ }
+
+ override fun contentLength(): Long = delegate.contentLength()
+
+ override fun contentType(): MediaType? = delegate.contentType()
+
+ override fun source(): BufferedSource {
+ return bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also {
+ bufferedSource = it
+ }
+ }
+
+ private class ProgressSource(
+ delegate: Source,
+ private val contentLength: Long,
+ private val progressState: MutableStateFlow,
+ ) : ForwardingSource(delegate) {
+
+ private var totalBytesRead = 0L
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ val bytesRead = super.read(sink, byteCount)
+ if (contentLength > 0) {
+ totalBytesRead += if (bytesRead != -1L) bytesRead else 0
+ progressState.value = (totalBytesRead.toDouble() / contentLength.toDouble()).toFloat()
+ }
+ return bytesRead
+ }
+ }
+}
diff --git a/app/src/main/res/layout-land/dialog_list_mode.xml b/app/src/main/res/layout-land/dialog_list_mode.xml
deleted file mode 100644
index f103a0ac3..000000000
--- a/app/src/main/res/layout-land/dialog_list_mode.xml
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_shelf_settings.xml b/app/src/main/res/layout/activity_shelf_settings.xml
new file mode 100644
index 000000000..a4c9781a8
--- /dev/null
+++ b/app/src/main/res/layout/activity_shelf_settings.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_list_mode.xml b/app/src/main/res/layout/dialog_list_mode.xml
index af859eb04..056ab4d2f 100644
--- a/app/src/main/res/layout/dialog_list_mode.xml
+++ b/app/src/main/res/layout/dialog_list_mode.xml
@@ -1,75 +1,103 @@
-
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical">
-
+ app:title="@string/options" />
-
+
+
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/margin_normal">
-
+ android:layout_marginHorizontal="@dimen/margin_normal"
+ android:layout_marginTop="@dimen/margin_normal"
+ android:text="@string/list_mode"
+ android:textAppearance="?textAppearanceTitleSmall" />
-
+ android:layout_marginHorizontal="@dimen/margin_normal"
+ android:layout_marginTop="@dimen/margin_small"
+ android:baselineAligned="false"
+ android:orientation="horizontal"
+ app:selectionRequired="true"
+ app:singleSelection="true">
-
+
+
+
+
+
+
+
+
+ android:layout_marginHorizontal="@dimen/margin_normal"
+ android:layout_marginTop="@dimen/margin_normal"
+ android:singleLine="true"
+ android:text="@string/grid_size"
+ android:textAppearance="?textAppearanceTitleSmall"
+ android:visibility="gone"
+ tools:visibility="visible" />
-
+
-
-
-
-
-
-
+
+
+
diff --git a/app/src/main/res/layout/item_shelf_section_draggable.xml b/app/src/main/res/layout/item_shelf_section_draggable.xml
new file mode 100644
index 000000000..dbf7b22b8
--- /dev/null
+++ b/app/src/main/res/layout/item_shelf_section_draggable.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/opt_shelf.xml b/app/src/main/res/menu/opt_shelf.xml
index 3cefebcb3..3a88eac5c 100644
--- a/app/src/main/res/menu/opt_shelf.xml
+++ b/app/src/main/res/menu/opt_shelf.xml
@@ -12,7 +12,7 @@
-
+
+
+
- %1$d فصل جديد
+
+
+
+ - %1$d فصول جديدة
+
+
+
+
+ - %1$d فصل
+ - %1$d فصلين
+ - %1$d بعض فصول
+ - %1$d عدة فصول
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
new file mode 100644
index 000000000..911680634
--- /dev/null
+++ b/app/src/main/res/values-ar/strings.xml
@@ -0,0 +1,57 @@
+
+
+ تفاصيل القائمة
+ حدث خطأ
+ تفاصيل
+ شبكة
+ وضع القائمة
+ إعدادات
+ المصادر البعيدة
+ غلق قائمة
+ فتح قائمة
+ فصول
+ المفضلة
+ تعذر الاتصال بالإنترنت
+ جار التحميل…
+ فصل %1$d في %2$d
+ غلق
+ حاول مجدداً
+ جاري الحوسبة …
+ التخزين المحلي
+ سجل
+ قائمة
+ محو سجل
+ ضع هذا في المفضلة
+ أضف
+ أدخل اسم القائمة
+ حفظ
+ لا سجل بعد
+ التحميلات
+ اسم
+ الأحدث
+ تقييم
+ صفحات
+ اقرأ
+ شارك
+ لم يتم عثور على اي شيء
+ لا مفضلة بعد
+ بحث
+ البحث في المانجا
+ جاري التنزيل…
+ انشاء اختصار…
+ مظهر
+ حسب النظام
+ شارك %s
+ في طور معالجة…
+ محدث
+ فلتر
+ ترتيب الفرز
+ ضوء
+ داكن
+ أزل
+ ازالة
+ شائع
+ قائمة جديدة
+ تم التنزيل
+ هل تريد محو سجل القراءة بالكامل بشكل دائم؟
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index e153d2e71..0354d02f7 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -266,7 +266,7 @@
Genres ausschließen
Geben Sie Genres an, die Sie nicht in den Vorschlägen sehen möchten
Ausgewählte Elemente dauerhaft vom Gerät löschen\?
- Sind Sie sicher, dass Sie alle ausgewählten Mangas mit allen Kapiteln herunterladen möchten\? Diese Aktion kann eine Menge Datenverkehr und Speicherplatz verbrauchen
+ Alle ausgewählten Mangas und ihre Kapitel herunterladen\? Das kann eine Menge Datenverkehr und Speicherplatz verbrauchen.
Entfernung abgeschlossen
Download-Verzögerung
Parallele Downloads
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index a34fedbab..150053d37 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -386,4 +386,8 @@
Restablecer
Tienes cambios sin guardar. ¿Quieres guardarlos o descartarlos\?
Descartar
+ Sin espacio en dispositivo
+ Zoom de webtoon
+ Permitir el gesto de acercamiento/alejamiento en modo webtoon (beta)
+ Mostrar el deslizador de cambio de página
\ 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 e3d6524db..cd3975284 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -267,7 +267,7 @@
Spécifiez les genres que vous ne voulez pas voir apparaître dans les suggestions
Supprimer définitivement les éléments sélectionnés de l\'appareil \?
Suppression terminée
- Voulez-vous vraiment télécharger tous les mangas sélectionnés avec tous leurs chapitres \? Cette action peut consommer beaucoup de trafic et de stockage
+ Télécharger tous les mangas sélectionnés et leurs chapitres \? Ceci peut consommer beaucoup de trafic et de stockage.
Téléchargements parallèles
Ralentissement du téléchargement
Permet d\'éviter le blocage de votre adresse IP
@@ -374,7 +374,7 @@
Importation terminée
Vous pouvez supprimer le fichier original du stockage pour gagner de l\'espace
L\'importation va bientôt commencer
- Vous avez des modifications non sauvegardées, voulez-vous les sauvegarder ou les abandonner \?
+ Sauvegarde ou abandon des modifications non sauvegardées \?
Abandonner
Rendre les mangas récents disponibles en appuyant longuement sur l\'icône de l\'application
Taper sur le bord droit ou appuyer sur la touche droite permet toujours de passer à la page suivante
@@ -387,4 +387,12 @@
Les paramètres de couleurs choisis seront sauvegardés pour ce manga
Il n\'y a plus d\'espace sur l\'appareil
Afficher le curseur de changement de page
+ Autoriser les gestes de zoom avant/arrière en mode webtoon (bêta)
+ Zoom Webtoon
+ Activez le Wi-Fi ou le réseau mobile pour lire les mangas en ligne
+ Différentes langues
+ Le réseau n\'est pas disponible
+ Compact
+ Erreur côté serveur (%1$d). Veuillez réessayer plus tard
+ Effacer aussi les informations sur les nouveaux chapitres
\ 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 426498448..1117570fe 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -267,7 +267,7 @@
サジェストで表示したくないジャンルを指定
選択した項目をデバイスから完全に削除しますか?
削除が完了しました
- 本当に選択したマンガを全編ダウンロードしますか?この動作は多くのトラフィックとストレージを消費する可能性があります
+ 選択したマンガとそのチャプターをすべてダウンロードしますか?これは、多くのトラフィックとストレージを消費する可能性があります。
IPアドレスのブロックを回避することができます
保存されたマンガの処理
ダウンロードの速度低下
@@ -383,9 +383,16 @@
輝度
コントラスト
リセット
- 未保存の変更がありますが、保存しますか、それとも破棄しますか?
+ 未保存の変更を保存または破棄しますか\?
選択した色の設定は、この漫画のために記憶されます
破棄
デバイスに空き容量がありません
ページ切り替えスライダーを表示
+ サーバーサイドエラー (%1$d) です。後で再試行してください
+ 新しいチャプターの情報も明確に
+ さまざまな言語
+ ネットワークが利用できません
+ Wi-Fiまたはモバイルネットワークをオンにして、オンラインでマンガを読むことができます
+ Webtoonズーム
+ ウェブトゥーンモードでズームイン/ズームアウトのジェスチャーを可能にする(ベータ版)
\ 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
deleted file mode 100644
index 56e1ed965..000000000
--- a/app/src/main/res/values-large/themes.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
\ 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 ba5ce1122..6d9e734d4 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -331,4 +331,12 @@
Lysstyrke
Fargekorrigering
Lagre eller forkast ulagrede endringer\?
+ Kompakt
+ Skru av alle
+ Bruk fingeravtrykk hvis tilgjengelig
+ Skru på Wi-Fi eller mobilnettverk for å lese manga på nett
+ Skru på merknader
+ Vis indikatorer for leseforløp
+ Datasletting
+ Avsluttingsbekreftelse
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 8241e3ac4..ef901bd78 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -8,5 +8,6 @@
#66FFFFFF
#29FFFFFF
#1FFFFFFF
+ #260052
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index f7d8f6eaf..3d4a096bf 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -266,7 +266,7 @@
Укажите жанры, которые Вы не хотите видеть в рекомендациях
Удалить выбранную мангу с накопителя?
Удаление завершено
- Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе
+ Загрузить выбранную мангу со всеми главами\? Это может привести к большому расходу трафика и места на накопителе.
Загружать параллельно
Замедление загрузки
Помогает избежать блокировки IP-адреса
@@ -376,14 +376,24 @@
Загрузка манги
Информация об ошибке:<br><tt>%1$s</tt><br><br>1. Попробуйте <a href=%2$s>открыть мангу в веб-браузере</a>, чтобы убедиться, что она доступна в источнике<br>2. Если она доступна, отправьте отчёт об ошибке разработчикам.
Показывать ярлыки последней прочитанной манги
- Сделать недавно прочитанную доступной по долгому нажатию на иконку приложения
+ Сделать недавно прочитанную мангу доступной по долгому нажатию на иконку приложения
Нажатие на правый край или нажатие правой клавиши всегда переключает на следующую страницу
Эргономичное управление режимом чтения
Сбросить
Отменить
- У вас есть несохраненные изменения, вы хотите сохранить или отменить их\?
+ Сохранить или отменить несохранённые изменения\?
Контрастность
Яркость
Цветокоррекция
- Выбранные настройки цвета запомнятся для этой манги
+ Выбранные настройки цвета будут сохранены для этой манги
+ Не осталось свободного места на накопителе
+ Масштабирование в режиме манхвы
+ Позволяет масштабировать страницы в режиме манхвы (бета)
+ Показывать слайдер переключения страниц
+ Включите Wi-Fi или передачу данных, чтобы читать мангу онлайн
+ Разные языки
+ Сеть недоступна
+ Также очистить информацию о новых главах
+ Внутренняя ошибка сервера (%1$d). Повторите попытку позже
+ Компактно
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index e06ccd0f8..8834a5dee 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -266,7 +266,7 @@
Türleri hariç tut
Önerilerde görmek istemediğiniz türleri belirtin
Seçilen ögeler aygıttan kalıcı olarak silinsin mi\?
- Seçilen tüm mangaları tüm bölümleriyle birlikte indirmek istediğinizden emin misiniz\? Bu işlem çok fazla trafik ve depolama alanı tüketebilir
+ Seçilen tüm manga ve bölümleri indirilsin mi\? Bu, çok fazla trafik ve depolama tüketebilir.
Kaldırma tamamlandı
Bölümler arka planda kaldırılacaktır. Bu biraz zaman alabilir
Paralel indirmeler
@@ -372,7 +372,7 @@
İçe aktarım tamamlandı
Yer açmak için orijinal dosyayı depolamadan silebilirsiniz
İçe aktarım birazdan başlayacak
- Feed
+ Akış
En son manga kısayollarını göster
Ergonomik okuyucu kontrol
Renk düzeltme
@@ -380,6 +380,19 @@
Kontrast
Sıfırla
Seçilen renk ayarları bu manga için hatırlanacaktır
- Kaydedilmemiş değişiklikleriniz var, kaydetmek mi istersiniz yoksa, yoksaymak mı istersiniz\?
+ Kaydedilmeyen değişiklikler kaydedilsin mi yoksa atılsın mı\?
Yoksay
+ Cihazda yer yok
+ Webtoon yakınlaştırma
+ Webtoon modunda yakınlaştırma hareketine izin ver (beta)
+ Sayfa değiştirme kaydırıcısını göster
+ Ayrıca yeni bölümler hakkındaki bilgileri temizle
+ Sıkı
+ Farklı diller
+ Ağ kullanılamıyor
+ Çevrim içi manga okumak için Wi-Fi veya mobil ağı açın
+ Sunucu tarafı hatası (%1$d). Lütfen daha sonra tekrar deneyin
+ Kaydedilen mangalar
+ Uygulama simgesine uzun basarak son mangaları kullanılabilir hale getirin
+ Sağ kenara dokunulduğunda veya sağ tuşa basıldığında her zaman bir sonraki sayfaya geçilir
\ 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
index 1b47f743e..0ced943b7 100644
--- a/app/src/main/res/values-uk/plurals.xml
+++ b/app/src/main/res/values-uk/plurals.xml
@@ -2,7 +2,7 @@
- %1$d новий розділ
- - %1$d нові розділи
+ - %1$d нових розділи
- %1$d нових розділів
- %1$d нових розділів
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 79260b7e1..53d9b821a 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -211,7 +211,7 @@
Оновлення пропозицій
Видалити вибрані елементи з пристрою назавжди\?
Видалення завершено
- Ви впевнені, що хочете завантажити всю вибрану манґу з усіма її розділами\? Це може споживати багато трафіку та пам’яті
+ Завантажити всю вибрану манґу з усіма її розділами\? Це може споживати багато трафіку та пам’яті.
Завантажувати паралельно
Сповільнення завантаження
Обробка збереженої манґи
@@ -292,7 +292,7 @@
Видалено з історії
DNS через HTTPS
Режим за замовчуванням
- Автоматично визначати, чи є манга вебтуном
+ Автоматично визначати, чи є манґа вебтуном
Автовизначення режиму читання
Вимкнути оптимізацію акумулятора
Допомагає з перевірками фонових оновлень
@@ -383,6 +383,16 @@
Контрастність
Скинути
Вибрані параметри кольору будуть запам\'ятовані для цієї манґи
- У вас є незбережені зміни, ви хочете зберегти чи скасувати їх\?
+ Зберегти чи скасувати незбережені зміни\?
Скасувати
+ Помилка на стороні сервера (%1$d). Будь ласка спробуйте пізніше
+ Також очистити інформацію про нові розділи
+ На пристрої не залишилося вільного місця
+ Різні мови
+ Мережа недоступна
+ Увімкніть Wi-Fi або мобільну мережу, щоб читати манґу онлайн
+ Масштабування в режимі вебтуну
+ Дозволити жести збільшення/зменшення масштабу в режимі вебтуну (бета)
+ Відображати повзунок перемикання сторінок
+ Компактно
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 607f5feca..d38116018 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -383,8 +383,16 @@
亮度
对比度
所选颜色设置将会应用于此漫画
- 您有未保存的更改,您希望存储还是放弃更改?
+ 保存还是放弃未保存的更改?
放弃
设备上没有剩余空间
显示换页滑块
+ Webtoon 缩放
+ 在 webtoon 模式下允许缩小/放大(测试)
+ 不同语言
+ 网络不可用
+ 打开 Wi-Fi 或移动网络在线阅读漫画
+ 同样清除新章节信息
+ 服务器端错误 (%1$d)。请稍后再试
+ 紧凑
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index a066547bb..bd13e4c65 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -21,5 +21,5 @@
#66000000
#29000000
#1F000000
-
+ #334800E0
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3f92c100..6e34f6fa9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -395,5 +395,8 @@
Different languages
Network is not available
Turn on Wi-Fi or mobile network to read manga online
+ Server side error (%1$d). Please try again later
+ Also clear information about new chapters
+ Compact
MyAnimeList
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 1f67c8f18..5b27f6044 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -86,8 +86,6 @@
-
-
diff --git a/app/src/release/java/org/koitharu/kotatsu/core/parser/DummyParser.kt b/app/src/release/java/org/koitharu/kotatsu/core/parser/DummyParser.kt
new file mode 100644
index 000000000..10779ef79
--- /dev/null
+++ b/app/src/release/java/org/koitharu/kotatsu/core/parser/DummyParser.kt
@@ -0,0 +1,39 @@
+package org.koitharu.kotatsu.core.parser
+
+import org.koitharu.kotatsu.parsers.MangaLoaderContext
+import org.koitharu.kotatsu.parsers.MangaParser
+import org.koitharu.kotatsu.parsers.config.ConfigKey
+import org.koitharu.kotatsu.parsers.exception.NotFoundException
+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.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaTag
+import org.koitharu.kotatsu.parsers.model.SortOrder
+import java.util.EnumSet
+
+class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) {
+
+ override val configKeyDomain: ConfigKey.Domain
+ get() = ConfigKey.Domain("localhost", null)
+
+ override val sortOrders: Set
+ get() = EnumSet.allOf(SortOrder::class.java)
+
+ override suspend fun getDetails(manga: Manga): Manga = stub()
+
+ override suspend fun getList(
+ offset: Int,
+ query: String?,
+ tags: Set?,
+ sortOrder: SortOrder,
+ ): List = stub()
+
+ override suspend fun getPages(chapter: MangaChapter): List = stub()
+
+ override suspend fun getTags(): Set = stub()
+
+ private fun stub(): Nothing {
+ throw NotFoundException("Usage of Dummy parser in release build", "")
+ }
+}
diff --git a/app/src/release/java/org/koitharu/kotatsu/core/parser/MangaParser.kt b/app/src/release/java/org/koitharu/kotatsu/core/parser/MangaParser.kt
deleted file mode 100644
index a8f4377e5..000000000
--- a/app/src/release/java/org/koitharu/kotatsu/core/parser/MangaParser.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.core.parser
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaParser
-import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.koitharu.kotatsu.parsers.newParser
-
-fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
- return source.newParser(loaderContext)
-}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 61f163ff5..0f59129a3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,8 +5,8 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.3.0'
- classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10'
+ classpath 'com.android.tools.build:gradle:7.3.1'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.44'
// NOTE: Do not place your application dependencies here; they belong