Refactor and optimization
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<B : ViewBinding> : BaseActivity<B>(),
|
||||
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
BaseActivity<B>(),
|
||||
View.OnSystemUiVisibilityChangeListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,5 @@ import org.koin.dsl.module
|
||||
|
||||
val githubModule
|
||||
get() = module {
|
||||
factory {
|
||||
GithubRepository(get())
|
||||
}
|
||||
factory { GithubRepository(get()) }
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import androidx.lifecycle.liveData
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
fun <T> 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 <T> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
|
||||
|
||||
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String?>(null)
|
||||
|
||||
val onShowToast = SingleLiveEvent<Int>()
|
||||
|
||||
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 newChapters = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { mangaId ->
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
// Remote manga for saved and saved for remote
|
||||
private val relatedManga = MutableStateFlow<Manga?>(null)
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
|
||||
private val chaptersReversed = settings.observe()
|
||||
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
|
||||
.map { settings.chaptersReverse }
|
||||
.onStart { emit(settings.chaptersReverse) }
|
||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
val manga = mangaData.filterNotNull()
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val favouriteCategories = favourite
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val newChaptersCount = newChapters
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val readingHistory = history
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
val isChaptersReversed = chaptersReversed
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
private val newChapters = viewModelScope.async(Dispatchers.Default) {
|
||||
trackingRepository.getNewChaptersCount(delegate.mangaId)
|
||||
}
|
||||
|
||||
val bookmarks = mangaData.flatMapLatest {
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
|
||||
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
||||
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
||||
val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) }
|
||||
val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
|
||||
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
val bookmarks = delegate.manga.flatMapLatest {
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||
|
||||
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<MangaChapter>,
|
||||
downloadedChapters: List<MangaChapter>?,
|
||||
currentId: Long?,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
): List<ChapterListItem> {
|
||||
val result = ArrayList<ChapterListItem>(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<MangaChapter>,
|
||||
sourceChapters: List<MangaChapter>,
|
||||
currentId: Long?,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
): List<ChapterListItem> {
|
||||
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
||||
val result = ArrayList<ChapterListItem>(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<MangaChapter>?): 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<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
|
||||
|
||||
@@ -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<String?>(null)
|
||||
// Remote manga for saved and saved for remote
|
||||
val relatedManga = MutableStateFlow<Manga?>(null)
|
||||
val manga: StateFlow<Manga?>
|
||||
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<ChapterListItem> {
|
||||
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<MangaChapter>,
|
||||
downloadedChapters: List<MangaChapter>?,
|
||||
currentId: Long?,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
): List<ChapterListItem> {
|
||||
val result = ArrayList<ChapterListItem>(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<MangaChapter>,
|
||||
sourceChapters: List<MangaChapter>,
|
||||
currentId: Long?,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
): List<ChapterListItem> {
|
||||
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
||||
val result = ArrayList<ChapterListItem>(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<MangaChapter>?): 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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ActivityDownloadsBinding>() {
|
||||
|
||||
@@ -28,11 +26,10 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Boolean>()
|
||||
|
||||
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(
|
||||
|
||||
@@ -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<List<ListModel>>
|
||||
val listMode = MutableLiveData<ListMode>()
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<List<@JvmSuppressWildcards Uri>> {
|
||||
@@ -68,9 +68,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
|
||||
try {
|
||||
importCall.launch(arrayOf("*/*"))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
e.printStackTraceDebug()
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
R.string.operation_not_supported,
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.ui
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -10,7 +11,6 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.IOException
|
||||
|
||||
class LocalListViewModel(
|
||||
private val repository: LocalMangaRepository,
|
||||
@@ -127,9 +127,7 @@ class LocalListViewModel(
|
||||
runCatching {
|
||||
repository.cleanup()
|
||||
}.onFailure { error ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
error.printStackTrace()
|
||||
}
|
||||
error.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Manga>()
|
||||
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()
|
||||
|
||||
@@ -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<MangaChapter>?) = 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,
|
||||
)
|
||||
@@ -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<ActivityReaderBinding>(),
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<out BaseReader<*>>>(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) }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Unit>()
|
||||
|
||||
@@ -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 <T> List<T>.trySublist(fromIndex: Int, toIndex: Int): List<T> {
|
||||
@@ -363,40 +358,23 @@ class ReaderViewModel(
|
||||
subList(fromIndexBounded, toIndexBounded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Manga.copy(chapters: List<MangaChapter>?) = 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<HistoryRepository>().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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Uri?> {
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 <I> ActivityResultLauncher<I>.tryLaunch(input: I, options: ActivityOptionsCo
|
||||
return runCatching {
|
||||
launch(input, options)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
|
||||
coroutineScope.launch {
|
||||
delay(delay)
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_pages_thumbs"
|
||||
android:icon="@drawable/ic_grid"
|
||||
android:title="@string/pages"
|
||||
android:visible="false"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
@@ -28,11 +30,13 @@
|
||||
android:id="@+id/action_reader_mode"
|
||||
android:icon="@drawable/ic_loading"
|
||||
android:title="@string/read_mode"
|
||||
android:visible="false"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_save_page"
|
||||
android:title="@string/save_page"
|
||||
android:visible="false"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
|
||||
inline fun Throwable.printStackTraceDebug() = Unit
|
||||
Reference in New Issue
Block a user