diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index a6fb1fbe4..2bcd23609 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -4,6 +4,9 @@
+
+
+
diff --git a/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt
new file mode 100644
index 000000000..e00bb6a83
--- /dev/null
+++ b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt
@@ -0,0 +1,3 @@
+package org.koitharu.kotatsu.utils.ext
+
+fun Throwable.printStackTraceDebug() = printStackTrace()
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt
index 03b0dd53b..6c481e467 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt
@@ -3,21 +3,21 @@ package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
+import java.io.File
+import java.io.InputStream
+import java.util.zip.ZipFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.medianOrNull
-import java.io.File
-import java.io.InputStream
-import java.util.zip.ZipFile
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
object MangaUtils : KoinComponent {
@@ -53,9 +53,7 @@ object MangaUtils : KoinComponent {
}
return size.width * 2 < size.height
} catch (e: Exception) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
return null
}
}
@@ -78,4 +76,4 @@ object MangaUtils : KoinComponent {
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt
index 64317e4a7..e43ca8877 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt
@@ -7,10 +7,12 @@ import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
+@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
@@ -18,7 +20,8 @@ private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
-abstract class BaseFullscreenActivity : BaseActivity(),
+abstract class BaseFullscreenActivity :
+ BaseActivity(),
View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity : BaseActivity(),
showSystemUI()
}
+ @Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java")
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
// TODO WindowInsetsControllerCompat works incorrect
+ @Suppress("DEPRECATION")
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
}
+ @Suppress("DEPRECATION")
protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
index 39233e28f..f4904f8ed 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
@@ -5,9 +5,9 @@ import androidx.lifecycle.viewModelScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.*
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
import org.koitharu.kotatsu.utils.SingleLiveEvent
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class BaseViewModel : ViewModel() {
@@ -34,9 +34,7 @@ abstract class BaseViewModel : ViewModel() {
}
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
- if (BuildConfig.DEBUG) {
- throwable.printStackTrace()
- }
+ throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
onError.postCall(throwable)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt
index 7da9e309f..58d8d22c6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt
@@ -4,7 +4,5 @@ import org.koin.dsl.module
val githubModule
get() = module {
- factory {
- GithubRepository(get())
- }
+ factory { GithubRepository(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
index 09557cb47..88304755b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
@@ -54,27 +54,23 @@ class VersionId(
return result
}
- companion object {
-
- private fun variantWeight(variantType: String) =
- when (variantType.lowercase(Locale.ROOT)) {
- "a", "alpha" -> 1
- "b", "beta" -> 2
- "rc" -> 4
- "" -> 8
- else -> 0
- }
-
- fun parse(versionName: String): VersionId {
- val parts = versionName.substringBeforeLast('-').split('.')
- val variant = versionName.substringAfterLast('-', "")
- return VersionId(
- major = parts.getOrNull(0)?.toIntOrNull() ?: 0,
- minor = parts.getOrNull(1)?.toIntOrNull() ?: 0,
- build = parts.getOrNull(2)?.toIntOrNull() ?: 0,
- variantType = variant.filter(Char::isLetter),
- variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0
- )
- }
+ private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
+ "a", "alpha" -> 1
+ "b", "beta" -> 2
+ "rc" -> 4
+ "" -> 8
+ else -> 0
}
+}
+
+fun VersionId(versionName: String): VersionId {
+ val parts = versionName.substringBeforeLast('-').split('.')
+ val variant = versionName.substringAfterLast('-', "")
+ return VersionId(
+ major = parts.getOrNull(0)?.toIntOrNull() ?: 0,
+ minor = parts.getOrNull(1)?.toIntOrNull() ?: 0,
+ build = parts.getOrNull(2)?.toIntOrNull() ?: 0,
+ variantType = variant.filter(Char::isLetter),
+ variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0,
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index 185336542..dd5f57cd5 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
@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.prefs
-import android.annotation.TargetApi
import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt
new file mode 100644
index 000000000..88c62514c
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt
@@ -0,0 +1,35 @@
+package org.koitharu.kotatsu.core.prefs
+
+import androidx.lifecycle.liveData
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.flow
+
+fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
+ var lastValue: T = valueProducer()
+ emit(lastValue)
+ observe().collect {
+ if (it == key) {
+ val value = valueProducer()
+ if (value != lastValue) {
+ emit(value)
+ }
+ lastValue = value
+ }
+ }
+}
+
+fun AppSettings.observeAsLiveData(
+ context: CoroutineContext,
+ key: String,
+ valueProducer: AppSettings.() -> T
+) = liveData(context) {
+ emit(valueProducer())
+ observe().collect {
+ if (it == key) {
+ val value = valueProducer()
+ if (value != latestValue) {
+ emit(value)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt
index bfc8b7b83..9ec51d479 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt
@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt
index 20a7bf0c3..fb3216cb2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import kotlin.system.exitProcess
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler {
@@ -13,7 +14,7 @@ class AppCrashHandler(private val applicationContext: Context) : Thread.Uncaught
try {
applicationContext.startActivity(intent)
} catch (t: Throwable) {
- t.printStackTrace()
+ t.printStackTraceDebug()
}
Log.e("CRASH", e.message, e)
exitProcess(1)
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index f6ecc6f0d..7762f512c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -191,7 +191,7 @@ class DetailsActivity :
R.id.action_save -> {
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
- val branches = viewModel.branches.value.orEmpty()
+ val branches = viewModel.branches.value?.toList().orEmpty()
if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches)
} else {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index c1624a6b8..31007f673 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -1,131 +1,106 @@
package org.koitharu.kotatsu.details.ui
-import androidx.core.os.LocaleListCompat
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
+import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
+import java.io.IOException
+import java.util.*
+import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
-import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
-import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
-import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.koitharu.kotatsu.parsers.util.mapToSet
-import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
-import org.koitharu.kotatsu.utils.ext.iterator
-import java.io.IOException
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class DetailsViewModel(
- private val intent: MangaIntent,
+ intent: MangaIntent,
private val historyRepository: HistoryRepository,
- private val favouritesRepository: FavouritesRepository,
+ favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository,
- private val mangaDataRepository: MangaDataRepository,
+ mangaDataRepository: MangaDataRepository,
private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings,
) : BaseViewModel() {
+ private val delegate = MangaDetailsDelegate(
+ intent = intent,
+ settings = settings,
+ mangaDataRepository = mangaDataRepository,
+ historyRepository = historyRepository,
+ localMangaRepository = localMangaRepository,
+ )
+
private var loadingJob: Job
- private val mangaData = MutableStateFlow(intent.manga)
- private val selectedBranch = MutableStateFlow(null)
val onShowToast = SingleLiveEvent()
- private val history = mangaData.mapNotNull { it?.id }
- .distinctUntilChanged()
- .flatMapLatest { mangaId ->
- historyRepository.observeOne(mangaId)
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
+ private val history = historyRepository.observeOne(delegate.mangaId)
+ .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
- private val favourite = mangaData.mapNotNull { it?.id }
- .distinctUntilChanged()
- .flatMapLatest { mangaId ->
- favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
+ private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
+ .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
- private val newChapters = mangaData.mapNotNull { it?.id }
- .distinctUntilChanged()
- .mapLatest { mangaId ->
- trackingRepository.getNewChaptersCount(mangaId)
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
+ private val newChapters = viewModelScope.async(Dispatchers.Default) {
+ trackingRepository.getNewChaptersCount(delegate.mangaId)
+ }
- // Remote manga for saved and saved for remote
- private val relatedManga = MutableStateFlow(null)
private val chaptersQuery = MutableStateFlow("")
- private val chaptersReversed = settings.observe()
- .filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
- .map { settings.chaptersReverse }
- .onStart { emit(settings.chaptersReverse) }
+ private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
- val manga = mangaData.filterNotNull()
- .asLiveData(viewModelScope.coroutineContext)
- val favouriteCategories = favourite
- .asLiveData(viewModelScope.coroutineContext)
- val newChaptersCount = newChapters
- .asLiveData(viewModelScope.coroutineContext)
- val readingHistory = history
- .asLiveData(viewModelScope.coroutineContext)
- val isChaptersReversed = chaptersReversed
- .asLiveData(viewModelScope.coroutineContext)
+ val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
+ val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
+ val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) }
+ val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
+ val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
- val bookmarks = mangaData.flatMapLatest {
+ val bookmarks = delegate.manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val onMangaRemoved = SingleLiveEvent()
- val branches = mangaData.map {
- it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
+ val branches = delegate.manga.map {
+ val chapters = it?.chapters ?: return@map emptySet()
+ chapters.mapTo(TreeSet()) { x -> x.branch }
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchIndex = combine(
branches.asFlow(),
- selectedBranch
+ delegate.selectedBranch
) { branches, selected ->
branches.indexOf(selected)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
- val isChaptersEmpty = mangaData.mapNotNull { m ->
- m?.run { chapters.isNullOrEmpty() }
+ val isChaptersEmpty = delegate.manga.map { m ->
+ m?.chapters?.isEmpty() == true
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
val chapters = combine(
combine(
- mangaData.map { it?.chapters.orEmpty() },
- relatedManga,
- history.map { it?.chapterId },
- newChapters,
- selectedBranch
- ) { chapters, related, currentId, newCount, branch ->
- val relatedChapters = related?.chapters
- if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
- mapChaptersWithSource(chapters, relatedChapters, currentId, newCount, branch)
- } else {
- mapChapters(chapters, relatedChapters, currentId, newCount, branch)
- }
+ delegate.manga,
+ delegate.relatedManga,
+ history,
+ delegate.selectedBranch,
+ ) { manga, related, history, branch ->
+ delegate.mapChapters(manga, related, history, newChapters.await(), branch)
},
chaptersReversed,
chaptersQuery,
@@ -134,7 +109,7 @@ class DetailsViewModel(
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchValue: String?
- get() = selectedBranch.value
+ get() = delegate.selectedBranch.value
init {
loadingJob = doLoad()
@@ -146,7 +121,11 @@ class DetailsViewModel(
}
fun deleteLocal() {
- val m = mangaData.value ?: return
+ val m = delegate.manga.value
+ if (m == null) {
+ onShowToast.call(R.string.file_not_found)
+ return
+ }
launchLoadingJob(Dispatchers.Default) {
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
@@ -171,11 +150,11 @@ class DetailsViewModel(
}
fun setSelectedBranch(branch: String?) {
- selectedBranch.value = branch
+ delegate.selectedBranch.value = branch
}
fun getRemoteManga(): Manga? {
- return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
+ return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
}
fun performChapterSearch(query: String?) {
@@ -183,7 +162,7 @@ class DetailsViewModel(
}
fun onDownloadComplete(downloadedManga: Manga) {
- val currentManga = mangaData.value ?: return
+ val currentManga = delegate.manga.value ?: return
if (currentManga.id != downloadedManga.id) {
return
}
@@ -194,142 +173,16 @@ class DetailsViewModel(
runCatching {
localMangaRepository.getDetails(downloadedManga)
}.onSuccess {
- relatedManga.value = it
+ delegate.relatedManga.value = it
}.onFailure {
- if (BuildConfig.DEBUG) {
- it.printStackTrace()
- }
+ it.printStackTraceDebug()
}
}
}
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
- var manga = mangaDataRepository.resolveIntent(intent)
- ?: throw MangaNotFoundException("Cannot find manga")
- mangaData.value = manga
- manga = MangaRepository(manga.source).getDetails(manga)
- // find default branch
- val hist = historyRepository.getOne(manga)
- selectedBranch.value = if (hist != null) {
- val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
- if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
- } else {
- predictBranch(manga.chapters)
- }
- mangaData.value = manga
- relatedManga.value = runCatching {
- if (manga.source == MangaSource.LOCAL) {
- val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
- MangaRepository(m.source).getDetails(m)
- } else {
- localMangaRepository.findSavedManga(manga)
- }
- }.onFailure { error ->
- if (BuildConfig.DEBUG) error.printStackTrace()
- }.getOrNull()
- }
-
- private fun mapChapters(
- chapters: List,
- downloadedChapters: List?,
- currentId: Long?,
- newCount: Int,
- branch: String?,
- ): List {
- val result = ArrayList(chapters.size)
- val dateFormat = settings.getDateFormat()
- val currentIndex = chapters.indexOfFirst { it.id == currentId }
- val firstNewIndex = chapters.size - newCount
- val downloadedIds = downloadedChapters?.mapToSet { it.id }
- for (i in chapters.indices) {
- val chapter = chapters[i]
- if (chapter.branch != branch) {
- continue
- }
- result += chapter.toListItem(
- isCurrent = i == currentIndex,
- isUnread = i > currentIndex,
- isNew = i >= firstNewIndex,
- isMissing = false,
- isDownloaded = downloadedIds?.contains(chapter.id) == true,
- dateFormat = dateFormat,
- )
- }
- return result
- }
-
- private fun mapChaptersWithSource(
- chapters: List,
- sourceChapters: List,
- currentId: Long?,
- newCount: Int,
- branch: String?,
- ): List {
- val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
- val result = ArrayList(sourceChapters.size)
- val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
- val firstNewIndex = sourceChapters.size - newCount
- val dateFormat = settings.getDateFormat()
- for (i in sourceChapters.indices) {
- val chapter = sourceChapters[i]
- val localChapter = chaptersMap.remove(chapter.id)
- if (chapter.branch != branch) {
- continue
- }
- result += localChapter?.toListItem(
- isCurrent = i == currentIndex,
- isUnread = i > currentIndex,
- isNew = i >= firstNewIndex,
- isMissing = false,
- isDownloaded = false,
- dateFormat = dateFormat,
- ) ?: chapter.toListItem(
- isCurrent = i == currentIndex,
- isUnread = i > currentIndex,
- isNew = i >= firstNewIndex,
- isMissing = true,
- isDownloaded = false,
- dateFormat = dateFormat,
- )
- }
- if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
- result.ensureCapacity(result.size + chaptersMap.size)
- chaptersMap.values.mapNotNullTo(result) {
- if (it.branch == branch) {
- it.toListItem(
- isCurrent = false,
- isUnread = true,
- isNew = false,
- isMissing = false,
- isDownloaded = false,
- dateFormat = dateFormat,
- )
- } else {
- null
- }
- }
- result.sortBy { it.chapter.number }
- }
- return result
- }
-
- private fun predictBranch(chapters: List?): String? {
- if (chapters.isNullOrEmpty()) {
- return null
- }
- val groups = chapters.groupBy { it.branch }
- for (locale in LocaleListCompat.getAdjustedDefault()) {
- var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
- if (groups.containsKey(language)) {
- return language
- }
- language = locale.getDisplayName(locale).toTitleCase(locale)
- if (groups.containsKey(language)) {
- return language
- }
- }
- return groups.maxByOrNull { it.value.size }?.key
+ delegate.doLoad()
}
private fun List.filterSearch(query: String): List {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
new file mode 100644
index 000000000..07f03dbda
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
@@ -0,0 +1,184 @@
+package org.koitharu.kotatsu.details.ui
+
+import androidx.core.os.LocaleListCompat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.koitharu.kotatsu.base.domain.MangaDataRepository
+import org.koitharu.kotatsu.base.domain.MangaIntent
+import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
+import org.koitharu.kotatsu.core.model.MangaHistory
+import org.koitharu.kotatsu.core.parser.MangaRepository
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem
+import org.koitharu.kotatsu.details.ui.model.toListItem
+import org.koitharu.kotatsu.history.domain.HistoryRepository
+import org.koitharu.kotatsu.local.domain.LocalMangaRepository
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.util.mapToSet
+import org.koitharu.kotatsu.parsers.util.toTitleCase
+import org.koitharu.kotatsu.utils.ext.iterator
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+
+class MangaDetailsDelegate(
+ private val intent: MangaIntent,
+ private val settings: AppSettings,
+ private val mangaDataRepository: MangaDataRepository,
+ private val historyRepository: HistoryRepository,
+ private val localMangaRepository: LocalMangaRepository,
+) {
+
+ private val mangaData = MutableStateFlow(intent.manga)
+
+ val selectedBranch = MutableStateFlow(null)
+ // Remote manga for saved and saved for remote
+ val relatedManga = MutableStateFlow(null)
+ val manga: StateFlow
+ get() = mangaData
+ val mangaId = intent.manga?.id ?: intent.mangaId
+
+ suspend fun doLoad() {
+ var manga = mangaDataRepository.resolveIntent(intent)
+ ?: throw MangaNotFoundException("Cannot find manga")
+ mangaData.value = manga
+ manga = MangaRepository(manga.source).getDetails(manga)
+ // find default branch
+ val hist = historyRepository.getOne(manga)
+ selectedBranch.value = if (hist != null) {
+ val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
+ if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
+ } else {
+ predictBranch(manga.chapters)
+ }
+ mangaData.value = manga
+ relatedManga.value = runCatching {
+ if (manga.source == MangaSource.LOCAL) {
+ val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
+ MangaRepository(m.source).getDetails(m)
+ } else {
+ localMangaRepository.findSavedManga(manga)
+ }
+ }.onFailure { error ->
+ error.printStackTraceDebug()
+ }.getOrNull()
+ }
+
+ fun mapChapters(
+ manga: Manga?,
+ related: Manga?,
+ history: MangaHistory?,
+ newCount: Int,
+ branch: String?,
+ ): List {
+ val chapters = manga?.chapters ?: return emptyList()
+ val relatedChapters = related?.chapters
+ return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
+ mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
+ } else {
+ mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
+ }
+ }
+
+ private fun mapChapters(
+ chapters: List,
+ downloadedChapters: List?,
+ currentId: Long?,
+ newCount: Int,
+ branch: String?,
+ ): List {
+ val result = ArrayList(chapters.size)
+ val dateFormat = settings.getDateFormat()
+ val currentIndex = chapters.indexOfFirst { it.id == currentId }
+ val firstNewIndex = chapters.size - newCount
+ val downloadedIds = downloadedChapters?.mapToSet { it.id }
+ for (i in chapters.indices) {
+ val chapter = chapters[i]
+ if (chapter.branch != branch) {
+ continue
+ }
+ result += chapter.toListItem(
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = false,
+ isDownloaded = downloadedIds?.contains(chapter.id) == true,
+ dateFormat = dateFormat,
+ )
+ }
+ return result
+ }
+
+ private fun mapChaptersWithSource(
+ chapters: List,
+ sourceChapters: List,
+ currentId: Long?,
+ newCount: Int,
+ branch: String?,
+ ): List {
+ val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
+ val result = ArrayList(sourceChapters.size)
+ val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
+ val firstNewIndex = sourceChapters.size - newCount
+ val dateFormat = settings.getDateFormat()
+ for (i in sourceChapters.indices) {
+ val chapter = sourceChapters[i]
+ val localChapter = chaptersMap.remove(chapter.id)
+ if (chapter.branch != branch) {
+ continue
+ }
+ result += localChapter?.toListItem(
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ ) ?: chapter.toListItem(
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = true,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ )
+ }
+ if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
+ result.ensureCapacity(result.size + chaptersMap.size)
+ chaptersMap.values.mapNotNullTo(result) {
+ if (it.branch == branch) {
+ it.toListItem(
+ isCurrent = false,
+ isUnread = true,
+ isNew = false,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ )
+ } else {
+ null
+ }
+ }
+ result.sortBy { it.chapter.number }
+ }
+ return result
+ }
+
+ private fun predictBranch(chapters: List?): String? {
+ if (chapters.isNullOrEmpty()) {
+ return null
+ }
+ val groups = chapters.groupBy { it.branch }
+ for (locale in LocaleListCompat.getAdjustedDefault()) {
+ var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
+ if (groups.containsKey(language)) {
+ return language
+ }
+ language = locale.getDisplayName(locale).toTitleCase(locale)
+ if (groups.containsKey(language)) {
+ return language
+ }
+ }
+ return groups.maxByOrNull { it.value.size }?.key
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
index 58335ed31..d079eb51f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
@@ -12,7 +12,6 @@ import kotlinx.coroutines.sync.Semaphore
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.IOException
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -24,6 +23,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.waitForNetwork
import org.koitharu.kotatsu.utils.progress.ProgressJob
@@ -156,9 +156,7 @@ class DownloadManager(
outState.value = DownloadState.Cancelled(startId, manga, cover)
throw e
} catch (e: Throwable) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
outState.value = DownloadState.Error(startId, manga, cover, e)
} finally {
withContext(NonCancellable) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
index e249e4dc5..a0c6c63dd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
@@ -3,10 +3,8 @@ package org.koitharu.kotatsu.download.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
-import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.flatMapLatest
@@ -17,7 +15,7 @@ import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService
-import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection
+import org.koitharu.kotatsu.utils.bindServiceWithLifecycle
class DownloadsActivity : BaseActivity() {
@@ -28,11 +26,10 @@ class DownloadsActivity : BaseActivity() {
val adapter = DownloadsAdapter(lifecycleScope, get())
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
- LifecycleAwareServiceConnection.bindService(
- this,
- this,
- Intent(this, DownloadService::class.java),
- 0
+ bindServiceWithLifecycle(
+ owner = this,
+ service = Intent(this, DownloadService::class.java),
+ flags = 0,
).service.flatMapLatest { binder ->
(binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null)
}.onEach {
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
index 46dc79586..1e24d033f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
@@ -3,10 +3,11 @@ package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -70,9 +71,7 @@ class FavouritesCategoriesViewModel(
return result
}
- private fun observeAllCategoriesVisible() = settings.observe()
- .filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE }
- .map { settings.isAllFavouritesVisible }
- .onStart { emit(settings.isAllFavouritesVisible) }
- .distinctUntilChanged()
+ private fun observeAllCategoriesVisible() = settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) {
+ isAllFavouritesVisible
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt
index d840b783f..e64e36e5a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt
@@ -16,7 +16,7 @@ fun categoryAD(
clickListener.onItemClick(item.category, it)
}
@Suppress("ClickableViewAccessibility")
- binding.imageViewHandle.setOnTouchListener { v, event ->
+ binding.imageViewHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item.category, itemView)
} else {
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
index 42dd81e95..1e14aeeca 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
@@ -2,14 +2,15 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
-import java.util.*
-import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
@@ -19,6 +20,8 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst
+import java.util.*
+import java.util.concurrent.TimeUnit
class HistoryListViewModel(
private val repository: HistoryRepository,
@@ -29,11 +32,7 @@ class HistoryListViewModel(
val isGroupingEnabled = MutableLiveData()
- private val historyGrouping = settings.observe()
- .filter { it == AppSettings.KEY_HISTORY_GROUPING }
- .map { settings.historyGrouping }
- .onStart { emit(settings.historyGrouping) }
- .distinctUntilChanged()
+ private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping }
.onEach { isGroupingEnabled.postValue(it) }
override val content = combine(
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
index 20f768c2f..6adc8c0d2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
@@ -4,16 +4,14 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaGridModel
-import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
-import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
-import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
abstract class MangaListViewModel(
private val settings: AppSettings,
@@ -21,20 +19,15 @@ abstract class MangaListViewModel(
abstract val content: LiveData>
val listMode = MutableLiveData()
- val gridScale = settings.observe()
- .filter { it == AppSettings.KEY_GRID_SIZE }
- .map { settings.gridSize / 100f }
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) {
- settings.gridSize / 100f
- }
+ val gridScale = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_GRID_SIZE,
+ valueProducer = { gridSize / 100f },
+ )
open fun onRemoveFilterTag(tag: MangaTag) = Unit
- protected fun createListModeFlow() = settings.observe()
- .filter { it == AppSettings.KEY_LIST_MODE }
- .map { settings.listMode }
- .onStart { emit(settings.listMode) }
- .distinctUntilChanged()
+ protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.onEach {
if (listMode.value != it) {
listMode.postValue(it)
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt
index 86b72c738..c13fd3cfa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt
@@ -13,7 +13,7 @@ fun currentFilterAD(
val chipGroup = itemView as ChipsView
- chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data ->
+ chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { _, data ->
listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt
index acba2466c..0cbb4fad7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt
@@ -1,18 +1,18 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.annotation.WorkerThread
+import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
-import java.util.*
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class FilterCoordinator(
private val repository: RemoteMangaRepository,
@@ -113,7 +113,7 @@ class FilterCoordinator(
FilterItem.Sort(it, isSelected = it == state.sortOrder)
}
}
- if(allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
+ if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
list.add(FilterItem.Header(R.string.genres, state.tags.size))
tags.mapTo(list) {
FilterItem.Tag(it, isChecked = it in state.tags)
@@ -153,9 +153,7 @@ class FilterCoordinator(
runCatching {
repository.getTags()
}.onFailure { error ->
- if (BuildConfig.DEBUG) {
- error.printStackTrace()
- }
+ error.printStackTraceDebug()
}.getOrNull()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
index 4e5115ac8..fc2dbad03 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
@@ -15,11 +15,11 @@ import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.utils.ShareHelper
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.progress.Progress
class LocalListFragment : MangaListFragment(), ActivityResultCallback> {
@@ -68,9 +68,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback
- if (BuildConfig.DEBUG) {
- error.printStackTrace()
- }
+ error.printStackTraceDebug()
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
index f2b98d7e0..17a2c25b2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent
@@ -21,11 +22,11 @@ class MainViewModel(
val onOpenReader = SingleLiveEvent()
var defaultSection by settings::defaultSection
- val isSuggestionsEnabled = settings.observe()
- .filter { it == AppSettings.KEY_SUGGESTIONS }
- .onStart { emit("") }
- .map { settings.isSuggestionsEnabled }
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
+ val isSuggestionsEnabled = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_SUGGESTIONS,
+ valueProducer = { isSuggestionsEnabled }
+ )
val isResumeEnabled = historyRepository
.observeHasItems()
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt
new file mode 100644
index 000000000..289f44386
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt
@@ -0,0 +1,27 @@
+package org.koitharu.kotatsu.reader.data
+
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaChapter
+
+fun Manga.filterChapters(branch: String?): Manga {
+ if (chapters.isNullOrEmpty()) return this
+ return copy(chapters = chapters?.filter { it.branch == branch })
+}
+
+private fun Manga.copy(chapters: List?) = Manga(
+ id = id,
+ title = title,
+ altTitle = altTitle,
+ url = url,
+ publicUrl = publicUrl,
+ rating = rating,
+ isNsfw = isNsfw,
+ coverUrl = coverUrl,
+ tags = tags,
+ state = state,
+ author = author,
+ largeCoverUrl = largeCoverUrl,
+ description = description,
+ chapters = chapters,
+ source = source,
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
index c54ed94d6..e4a29b948 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
@@ -6,11 +6,13 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.*
-import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.core.graphics.Insets
-import androidx.core.view.*
-import androidx.fragment.app.commit
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.Slide
import androidx.transition.TransitionManager
@@ -37,11 +39,7 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
-import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
-import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
-import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
-import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity
@@ -51,6 +49,8 @@ import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
+import org.koitharu.kotatsu.utils.ext.postDelayed
+import java.util.concurrent.TimeUnit
class ReaderActivity :
BaseFullscreenActivity(),
@@ -75,13 +75,13 @@ class ReaderActivity :
private lateinit var controlDelegate: ReaderControlDelegate
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var gestureInsets: Insets = Insets.NONE
-
- private val reader
- get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReader<*>
+ private lateinit var readerManager: ReaderManager
+ private val hideUiRunnable = Runnable { setUiIsVisible(false) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityReaderBinding.inflate(layoutInflater))
+ readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
orientationHelper = ScreenOrientationHelper(this)
@@ -91,6 +91,7 @@ class ReaderActivity :
insetsDelegate.interceptingWindowInsetsListener = this
orientationHelper.observeAutoOrientation()
+ .flowWithLifecycle(lifecycle)
.onEach {
binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it
}.launchIn(lifecycleScope)
@@ -113,33 +114,20 @@ class ReaderActivity :
}
private fun onInitReader(mode: ReaderMode) {
- val currentReader = reader
- when (mode) {
- ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) {
- supportFragmentManager.commit {
- replace(R.id.container, WebtoonReaderFragment())
- }
- }
- ReaderMode.REVERSED -> if (currentReader !is ReversedReaderFragment) {
- supportFragmentManager.commit {
- replace(R.id.container, ReversedReaderFragment())
- }
- }
- ReaderMode.STANDARD -> if (currentReader !is PagerReaderFragment) {
- supportFragmentManager.commit {
- replace(R.id.container, PagerReaderFragment())
- }
- }
+ if (readerManager.currentMode != mode) {
+ readerManager.replace(mode)
}
- binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).setIcon(
- when (mode) {
- ReaderMode.WEBTOON -> R.drawable.ic_script
- ReaderMode.REVERSED -> R.drawable.ic_read_reversed
- ReaderMode.STANDARD -> R.drawable.ic_book_page
- }
- )
- binding.appbarTop.postDelayed(1000) {
- setUiIsVisible(false)
+ val iconRes = when (mode) {
+ ReaderMode.WEBTOON -> R.drawable.ic_script
+ ReaderMode.REVERSED -> R.drawable.ic_read_reversed
+ ReaderMode.STANDARD -> R.drawable.ic_book_page
+ }
+ binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).run {
+ setIcon(iconRes)
+ setVisible(true)
+ }
+ if (binding.appbarTop.isVisible) {
+ lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
}
}
@@ -151,18 +139,8 @@ class ReaderActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reader_mode -> {
- ReaderConfigDialog.show(
- supportFragmentManager,
- when (reader) {
- is PagerReaderFragment -> ReaderMode.STANDARD
- is WebtoonReaderFragment -> ReaderMode.WEBTOON
- is ReversedReaderFragment -> ReaderMode.REVERSED
- else -> {
- showWaitWhileLoading()
- return false
- }
- }
- )
+ val currentMode = readerManager.currentMode ?: return false
+ ReaderConfigDialog.show(supportFragmentManager, currentMode)
}
R.id.action_settings -> {
startActivity(SettingsActivity.newReaderSettingsIntent(this))
@@ -184,17 +162,17 @@ class ReaderActivity :
supportFragmentManager,
pages,
title?.toString().orEmpty(),
- reader?.getCurrentState()?.page ?: -1
+ readerManager.currentReader?.getCurrentState()?.page ?: -1,
)
} else {
- showWaitWhileLoading()
+ return false
}
}
R.id.action_save_page -> {
viewModel.getCurrentPage()?.also { page ->
- viewModel.saveCurrentState(reader?.getCurrentState())
+ viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.saveCurrentPage(page, savePageRequest)
- } ?: showWaitWhileLoading()
+ } ?: return false
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value == true) {
@@ -216,10 +194,14 @@ class ReaderActivity :
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
binding.layoutLoading.isVisible = isLoading && !hasPages
if (isLoading && hasPages) {
- binding.toastView.show(R.string.loading_, true)
+ binding.toastView.show(R.string.loading_)
} else {
binding.toastView.hide()
}
+ val menu = binding.toolbarBottom.menu
+ menu.findItem(R.id.action_bookmark).isVisible = hasPages
+ menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages
+ menu.findItem(R.id.action_save_page).isVisible = hasPages
}
private fun onError(e: Throwable) {
@@ -279,14 +261,14 @@ class ReaderActivity :
val index = pages.indexOfFirst { it.id == page.id }
if (index != -1) {
withContext(Dispatchers.Main) {
- reader?.switchPageTo(index, true)
+ readerManager.currentReader?.switchPageTo(index, true)
}
}
}
}
override fun onReaderModeChanged(mode: ReaderMode) {
- viewModel.saveCurrentState(reader?.getCurrentState())
+ viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.switchMode(mode)
}
@@ -304,12 +286,6 @@ class ReaderActivity :
}
}
- private fun showWaitWhileLoading() {
- Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply {
- setGravity(Gravity.CENTER, 0, 0)
- }.show()
- }
-
private fun setWindowSecure(isSecure: Boolean) {
if (isSecure) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@@ -358,7 +334,7 @@ class ReaderActivity :
override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun switchPageBy(delta: Int) {
- reader?.switchPageBy(delta)
+ readerManager.currentReader?.switchPageBy(delta)
}
override fun toggleUiVisibility() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt
index f8c5d73c0..dbe853894 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt
@@ -5,14 +5,16 @@ import android.view.SoundEffectConstants
import android.view.View
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.utils.GridTouchHelper
-@Suppress("UNUSED_PARAMETER")
class ReaderControlDelegate(
- private val scope: LifecycleCoroutineScope,
- private val settings: AppSettings,
+ scope: LifecycleCoroutineScope,
+ settings: AppSettings,
private val listener: OnInteractionListener
) {
@@ -20,12 +22,8 @@ class ReaderControlDelegate(
private var isVolumeKeysSwitchEnabled: Boolean = false
init {
- settings.observe()
- .filter { it == AppSettings.KEY_READER_SWITCHERS }
- .map { settings.readerPageSwitch }
- .onStart { emit(settings.readerPageSwitch) }
- .distinctUntilChanged()
- .flowOn(Dispatchers.IO)
+ settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch }
+ .flowOn(Dispatchers.Default)
.onEach {
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it
@@ -57,7 +55,7 @@ class ReaderControlDelegate(
}
}
- fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) {
+ fun onKeyDown(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean = when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
listener.switchPageBy(-1)
true
@@ -92,9 +90,11 @@ class ReaderControlDelegate(
else -> false
}
- fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
- return (isVolumeKeysSwitchEnabled &&
- (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP))
+ fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean {
+ return (
+ isVolumeKeysSwitchEnabled &&
+ (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)
+ )
}
interface OnInteractionListener {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt
new file mode 100644
index 000000000..c5497fe8a
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt
@@ -0,0 +1,45 @@
+package org.koitharu.kotatsu.reader.ui
+
+import androidx.annotation.IdRes
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.commit
+import org.koitharu.kotatsu.core.prefs.ReaderMode
+import org.koitharu.kotatsu.reader.ui.pager.BaseReader
+import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
+import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
+import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
+import java.util.*
+
+class ReaderManager(
+ private val fragmentManager: FragmentManager,
+ @IdRes private val containerResId: Int,
+) {
+
+ private val modeMap = EnumMap>>(ReaderMode::class.java)
+
+ init {
+ modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java
+ modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java
+ modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
+ }
+
+ val currentReader: BaseReader<*>?
+ get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*>
+
+ val currentMode: ReaderMode?
+ get() {
+ val readerClass = currentReader?.javaClass ?: return null
+ return modeMap.entries.find { it.value == readerClass }?.key
+ }
+
+ fun replace(newMode: ReaderMode) {
+ val readerClass = requireNotNull(modeMap[newMode])
+ fragmentManager.commit {
+ replace(containerResId, readerClass, null, null)
+ }
+ }
+
+ fun replace(reader: BaseReader<*>) {
+ fragmentManager.commit { replace(containerResId, reader) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt
index f9852f4c6..a2acc8df7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt
@@ -1,13 +1,11 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
-import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.isVisible
-import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.transition.Fade
import androidx.transition.Slide
import androidx.transition.TransitionManager
@@ -15,26 +13,28 @@ import androidx.transition.TransitionSet
import com.google.android.material.textview.MaterialTextView
class ReaderToastView @JvmOverloads constructor(
- context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
) : MaterialTextView(context, attrs, defStyleAttr) {
private var hideRunnable = Runnable {
hide()
}
- fun show(message: CharSequence, isLoading: Boolean) {
+ fun show(message: CharSequence) {
removeCallbacks(hideRunnable)
text = message
setupTransition()
isVisible = true
}
- fun show(@StringRes messageId: Int, isLoading: Boolean) {
- show(context.getString(messageId), isLoading)
+ fun show(@StringRes messageId: Int) {
+ show(context.getString(messageId))
}
fun showTemporary(message: CharSequence, duration: Long) {
- show(message, false)
+ show(message)
postDelayed(hideRunnable, duration)
}
@@ -49,7 +49,7 @@ class ReaderToastView @JvmOverloads constructor(
super.onDetachedFromWindow()
}
- private fun setupTransition () {
+ private fun setupTransition() {
val parentView = parent as? ViewGroup ?: return
val transition = TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
@@ -58,14 +58,4 @@ class ReaderToastView @JvmOverloads constructor(
.addTransition(Fade())
TransitionManager.beginDelayedTransition(parentView, transition)
}
-
- // FIXME use it as compound drawable
- private fun createProgressDrawable(): CircularProgressDrawable {
- val drawable = CircularProgressDrawable(context)
- drawable.setStyle(CircularProgressDrawable.DEFAULT)
- drawable.arrowEnabled = false
- drawable.setColorSchemeColors(Color.WHITE)
- drawable.centerRadius = lineHeight / 3f
- return drawable
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index f9ba0655f..784c844ff 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -8,9 +8,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.get
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
@@ -21,22 +18,25 @@ import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.core.prefs.ReaderMode
-import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
+import org.koitharu.kotatsu.core.prefs.*
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
+import org.koitharu.kotatsu.reader.data.filterChapters
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.SingleLiveEvent
-import org.koitharu.kotatsu.utils.ext.IgnoreErrors
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import java.util.*
+private const val BOUNDS_PAGE_OFFSET = 2
+private const val PAGES_TRIM_THRESHOLD = 120
+private const val PREFETCH_LIMIT = 10
+
class ReaderViewModel(
private val intent: MangaIntent,
initialState: ReaderState?,
@@ -78,22 +78,19 @@ class ReaderViewModel(
val manga: Manga?
get() = mangaData.value
- val readerAnimation = settings.observe()
- .filter { it == AppSettings.KEY_READER_ANIMATION }
- .map { settings.readerAnimation }
- .onStart { emit(settings.readerAnimation) }
- .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO)
+ val readerAnimation = settings.observeAsLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ key = AppSettings.KEY_READER_ANIMATION,
+ valueProducer = { readerAnimation }
+ )
val isScreenshotsBlockEnabled = combine(
mangaData,
- settings.observe()
- .filter { it == AppSettings.KEY_SCREENSHOTS_POLICY }
- .onStart { emit("") }
- .map { settings.screenshotsPolicy },
+ settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
) { manga, policy ->
policy == ScreenshotsPolicy.BLOCK_ALL ||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
- }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO)
+ }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val onZoomChanged = SingleLiveEvent()
@@ -142,7 +139,7 @@ class ReaderViewModel(
if (state != null) {
currentState.value = state
}
- saveState(
+ historyRepository.saveStateAsync(
mangaData.value ?: return,
state ?: currentState.value ?: return
)
@@ -169,9 +166,7 @@ class ReaderViewModel(
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
onPageSaved.postCall(null)
}
}
@@ -286,7 +281,7 @@ class ReaderViewModel(
}
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
- mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch })
+ mangaData.value = manga.filterChapters(branch)
readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
@@ -349,9 +344,9 @@ class ReaderViewModel(
private fun subscribeToSettings() {
settings.observe()
- .filter { it == AppSettings.KEY_ZOOM_MODE }
- .onEach { onZoomChanged.postCall(Unit) }
- .launchIn(viewModelScope + Dispatchers.IO)
+ .onEach { key ->
+ if (key == AppSettings.KEY_ZOOM_MODE) onZoomChanged.postCall(Unit)
+ }.launchIn(viewModelScope + Dispatchers.Default)
}
private fun List.trySublist(fromIndex: Int, toIndex: Int): List {
@@ -363,40 +358,23 @@ class ReaderViewModel(
subList(fromIndexBounded, toIndexBounded)
}
}
-
- private fun Manga.copy(chapters: List?) = Manga(
- id = id,
- title = title,
- altTitle = altTitle,
- url = url,
- publicUrl = publicUrl,
- rating = rating,
- isNsfw = isNsfw,
- coverUrl = coverUrl,
- tags = tags,
- state = state,
- author = author,
- largeCoverUrl = largeCoverUrl,
- description = description,
- chapters = chapters,
- source = source,
- )
-
- private companion object : KoinComponent {
-
- const val BOUNDS_PAGE_OFFSET = 2
- const val PAGES_TRIM_THRESHOLD = 120
- const val PREFETCH_LIMIT = 10
-
- fun saveState(manga: Manga, state: ReaderState) {
- processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) {
- get().addOrUpdate(
- manga = manga,
- chapterId = state.chapterId,
- page = state.page,
- scroll = state.scroll
- )
- }
+}
+
+/**
+ * This function is not a member of the ReaderViewModel
+ * because it should work independently of the ViewModel's lifecycle.
+ */
+private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState): Job {
+ return processLifecycleScope.launch(Dispatchers.Default) {
+ runCatching {
+ addOrUpdate(
+ manga = manga,
+ chapterId = state.chapterId,
+ page = state.page,
+ scroll = state.scroll
+ )
+ }.onFailure {
+ it.printStackTraceDebug()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
index b2a540baa..9c9922598 100644
--- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
@@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.*
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
@@ -21,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
private const val FILTER_MIN_INTERVAL = 750L
@@ -133,9 +133,7 @@ class RemoteListViewModel(
}
hasNextPage.value = list.isNotEmpty()
} catch (e: Throwable) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
listError.value = e
if (!mangaList.value.isNullOrEmpty()) {
onError.postCall(e)
@@ -158,4 +156,4 @@ class RemoteListViewModel(
textSecondary = 0,
actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter,
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
index 5b7bc661e..a9e2ab345 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
@@ -8,15 +8,6 @@ import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import java.io.ByteArrayInputStream
-import java.io.InputStream
-import java.security.MessageDigest
-import java.security.NoSuchAlgorithmException
-import java.security.cert.CertificateEncodingException
-import java.security.cert.CertificateException
-import java.security.cert.CertificateFactory
-import java.security.cert.X509Certificate
-import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
@@ -28,6 +19,16 @@ import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.utils.FileSize
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+import java.security.cert.CertificateEncodingException
+import java.security.cert.CertificateException
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
class AppUpdateChecker(private val activity: ComponentActivity) {
@@ -45,8 +46,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
suspend fun checkNow() = runCatching {
val version = repo.getLatestVersion()
- val newVersionId = VersionId.parse(version.name)
- val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
+ val newVersionId = VersionId(version.name)
+ val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
val result = newVersionId > currentVersionId
if (result) {
withContext(Dispatchers.Main) {
@@ -56,7 +57,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
settings.lastUpdateCheckTimestamp = System.currentTimeMillis()
result
}.onFailure {
- it.printStackTrace()
+ it.printStackTraceDebug()
}.getOrNull()
@MainThread
@@ -99,7 +100,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
return null
}
val signatures = packageInfo?.signatures
@@ -109,7 +110,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
return null
}
return try {
@@ -117,10 +118,10 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
null
} catch (e: CertificateEncodingException) {
- e.printStackTrace()
+ e.printStackTraceDebug()
null
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
index 384905df2..4ffa13c5b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
@@ -6,7 +6,6 @@ import androidx.preference.Preference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -14,6 +13,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -70,9 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
preference.title = getString(R.string.logged_in_as, username)
}.onFailure { error ->
preference.isEnabled = error is AuthRequiredException
- if (BuildConfig.DEBUG) {
- error.printStackTrace()
- }
+ error.printStackTraceDebug()
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
index b41ebb205..53ad51cbc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
@@ -7,12 +7,13 @@ import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
-class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
+class BackupSettingsFragment :
+ BasePreferenceFragment(R.string.backup_restore),
ActivityResultCallback {
private val backupSelectCall = registerForActivityResult(
@@ -34,9 +35,7 @@ class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
try {
backupSelectCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
+ e.printStackTraceDebug()
Snackbar.make(
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT
).show()
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt
index 03dd423ea..cedc875fa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt
@@ -10,7 +10,7 @@ import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-class LifecycleAwareServiceConnection private constructor(
+class LifecycleAwareServiceConnection(
private val host: Activity,
) : ServiceConnection, DefaultLifecycleObserver {
@@ -31,19 +31,15 @@ class LifecycleAwareServiceConnection private constructor(
super.onDestroy(owner)
host.unbindService(this)
}
-
- companion object {
-
- fun bindService(
- host: Activity,
- lifecycleOwner: LifecycleOwner,
- service: Intent,
- flags: Int,
- ): LifecycleAwareServiceConnection {
- val connection = LifecycleAwareServiceConnection(host)
- host.bindService(service, connection, flags)
- lifecycleOwner.lifecycle.addObserver(connection)
- return connection
- }
- }
+}
+
+fun Activity.bindServiceWithLifecycle(
+ owner: LifecycleOwner,
+ service: Intent,
+ flags: Int
+): LifecycleAwareServiceConnection {
+ val connection = LifecycleAwareServiceConnection(this)
+ bindService(service, connection, flags)
+ owner.lifecycle.addObserver(connection)
+ return connection
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
index 6f15f7cd3..bd6ad731b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
@@ -9,7 +9,11 @@ import android.net.Uri
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.core.app.ActivityOptionsCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
@@ -55,4 +59,11 @@ fun ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCo
return runCatching {
launch(input, options)
}.isSuccess
+}
+
+fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
+ coroutineScope.launch {
+ delay(delay)
+ runnable.run()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt
index 412ead77c..dd4907134 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt
@@ -3,15 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.CoroutineExceptionHandler
-import org.koitharu.kotatsu.BuildConfig
-
-val IgnoreErrors
- get() = CoroutineExceptionHandler { _, e ->
- if (BuildConfig.DEBUG) {
- e.printStackTrace()
- }
- }
val processLifecycleScope: LifecycleCoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt
index ad1fe8b39..e3cd66b43 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.liveData
+import kotlinx.coroutines.Deferred
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow
diff --git a/app/src/main/res/menu/opt_reader_bottom.xml b/app/src/main/res/menu/opt_reader_bottom.xml
index 978a29d74..569bffc11 100644
--- a/app/src/main/res/menu/opt_reader_bottom.xml
+++ b/app/src/main/res/menu/opt_reader_bottom.xml
@@ -9,12 +9,14 @@
android:id="@+id/action_bookmark"
android:icon="@drawable/ic_bookmark"
android:title="@string/bookmark_add"
+ android:visible="false"
app:showAsAction="always" />