Refactor and optimization

This commit is contained in:
Koitharu
2022-05-10 17:45:11 +03:00
parent 1cbb825892
commit 3e785a2555
41 changed files with 593 additions and 518 deletions

View File

@@ -4,6 +4,9 @@
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" /> <inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" /> <inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />

View File

@@ -0,0 +1,3 @@
package org.koitharu.kotatsu.utils.ext
fun Throwable.printStackTraceDebug() = printStackTrace()

View File

@@ -3,21 +3,21 @@ package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Size import android.util.Size
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.medianOrNull import org.koitharu.kotatsu.parsers.util.medianOrNull
import java.io.File import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.InputStream
import java.util.zip.ZipFile
object MangaUtils : KoinComponent { object MangaUtils : KoinComponent {
@@ -53,9 +53,7 @@ object MangaUtils : KoinComponent {
} }
return size.width * 2 < size.height return size.width * 2 < size.height
} catch (e: Exception) { } catch (e: Exception) {
if (BuildConfig.DEBUG) { e.printStackTraceDebug()
e.printStackTrace()
}
return null return null
} }
} }
@@ -78,4 +76,4 @@ object MangaUtils : KoinComponent {
check(imageHeight > 0 && imageWidth > 0) check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight) return Size(imageWidth, imageHeight)
} }
} }

View File

@@ -7,10 +7,12 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 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_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 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_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 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_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(), abstract class BaseFullscreenActivity<B : ViewBinding> :
BaseActivity<B>(),
View.OnSystemUiVisibilityChangeListener { View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
showSystemUI() showSystemUI()
} }
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
final override fun onSystemUiVisibilityChange(visibility: Int) { final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
} }
// TODO WindowInsetsControllerCompat works incorrect // TODO WindowInsetsControllerCompat works incorrect
@Suppress("DEPRECATION")
protected fun hideSystemUI() { protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
} }
@Suppress("DEPRECATION")
protected fun showSystemUI() { protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
} }

View File

@@ -5,9 +5,9 @@ import androidx.lifecycle.viewModelScope
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class BaseViewModel : ViewModel() { abstract class BaseViewModel : ViewModel() {
@@ -34,9 +34,7 @@ abstract class BaseViewModel : ViewModel() {
} }
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
if (BuildConfig.DEBUG) { throwable.printStackTraceDebug()
throwable.printStackTrace()
}
if (throwable !is CancellationException) { if (throwable !is CancellationException) {
onError.postCall(throwable) onError.postCall(throwable)
} }

View File

@@ -4,7 +4,5 @@ import org.koin.dsl.module
val githubModule val githubModule
get() = module { get() = module {
factory { factory { GithubRepository(get()) }
GithubRepository(get())
}
} }

View File

@@ -54,27 +54,23 @@ class VersionId(
return result return result
} }
companion object { private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
"a", "alpha" -> 1
private fun variantWeight(variantType: String) = "b", "beta" -> 2
when (variantType.lowercase(Locale.ROOT)) { "rc" -> 4
"a", "alpha" -> 1 "" -> 8
"b", "beta" -> 2 else -> 0
"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
)
}
} }
}
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,
)
} }

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.prefs package org.koitharu.kotatsu.core.prefs
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager import android.net.ConnectivityManager

View File

@@ -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)
}
}
}
}

View File

@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
fun valueOf(id: Int) = values().firstOrNull { it.id == id } fun valueOf(id: Int) = values().firstOrNull { it.id == id }
} }
} }

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler { class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler {
@@ -13,7 +14,7 @@ class AppCrashHandler(private val applicationContext: Context) : Thread.Uncaught
try { try {
applicationContext.startActivity(intent) applicationContext.startActivity(intent)
} catch (t: Throwable) { } catch (t: Throwable) {
t.printStackTrace() t.printStackTraceDebug()
} }
Log.e("CRASH", e.message, e) Log.e("CRASH", e.message, e)
exitProcess(1) exitProcess(1)

View File

@@ -191,7 +191,7 @@ class DetailsActivity :
R.id.action_save -> { R.id.action_save -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0 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) { if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches) showSaveConfirmation(it, chaptersCount, branches)
} else { } else {

View File

@@ -1,131 +1,106 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import java.io.IOException
import kotlinx.coroutines.Job import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* 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.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository 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.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.ui.model.ChapterListItem 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.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.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.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.IOException
class DetailsViewModel( class DetailsViewModel(
private val intent: MangaIntent, intent: MangaIntent,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository, favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val mangaDataRepository: MangaDataRepository, mangaDataRepository: MangaDataRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
private val delegate = MangaDetailsDelegate(
intent = intent,
settings = settings,
mangaDataRepository = mangaDataRepository,
historyRepository = historyRepository,
localMangaRepository = localMangaRepository,
)
private var loadingJob: Job private var loadingJob: Job
private val mangaData = MutableStateFlow(intent.manga)
private val selectedBranch = MutableStateFlow<String?>(null)
val onShowToast = SingleLiveEvent<Int>() val onShowToast = SingleLiveEvent<Int>()
private val history = mangaData.mapNotNull { it?.id } private val history = historyRepository.observeOne(delegate.mangaId)
.distinctUntilChanged() .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
.flatMapLatest { mangaId ->
historyRepository.observeOne(mangaId)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private val favourite = mangaData.mapNotNull { it?.id } private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
.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) }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val manga = mangaData.filterNotNull() private val newChapters = viewModelScope.async(Dispatchers.Default) {
.asLiveData(viewModelScope.coroutineContext) trackingRepository.getNewChaptersCount(delegate.mangaId)
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 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()) if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val onMangaRemoved = SingleLiveEvent<Manga>() val onMangaRemoved = SingleLiveEvent<Manga>()
val branches = mangaData.map { val branches = delegate.manga.map {
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty() val chapters = it?.chapters ?: return@map emptySet()
chapters.mapTo(TreeSet()) { x -> x.branch }
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchIndex = combine( val selectedBranchIndex = combine(
branches.asFlow(), branches.asFlow(),
selectedBranch delegate.selectedBranch
) { branches, selected -> ) { branches, selected ->
branches.indexOf(selected) branches.indexOf(selected)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val isChaptersEmpty = mangaData.mapNotNull { m -> val isChaptersEmpty = delegate.manga.map { m ->
m?.run { chapters.isNullOrEmpty() } m?.chapters?.isEmpty() == true
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
val chapters = combine( val chapters = combine(
combine( combine(
mangaData.map { it?.chapters.orEmpty() }, delegate.manga,
relatedManga, delegate.relatedManga,
history.map { it?.chapterId }, history,
newChapters, delegate.selectedBranch,
selectedBranch ) { manga, related, history, branch ->
) { chapters, related, currentId, newCount, branch -> delegate.mapChapters(manga, related, history, newChapters.await(), 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)
}
}, },
chaptersReversed, chaptersReversed,
chaptersQuery, chaptersQuery,
@@ -134,7 +109,7 @@ class DetailsViewModel(
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchValue: String? val selectedBranchValue: String?
get() = selectedBranch.value get() = delegate.selectedBranch.value
init { init {
loadingJob = doLoad() loadingJob = doLoad()
@@ -146,7 +121,11 @@ class DetailsViewModel(
} }
fun deleteLocal() { 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) { launchLoadingJob(Dispatchers.Default) {
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m) val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
@@ -171,11 +150,11 @@ class DetailsViewModel(
} }
fun setSelectedBranch(branch: String?) { fun setSelectedBranch(branch: String?) {
selectedBranch.value = branch delegate.selectedBranch.value = branch
} }
fun getRemoteManga(): Manga? { fun getRemoteManga(): Manga? {
return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
} }
fun performChapterSearch(query: String?) { fun performChapterSearch(query: String?) {
@@ -183,7 +162,7 @@ class DetailsViewModel(
} }
fun onDownloadComplete(downloadedManga: Manga) { fun onDownloadComplete(downloadedManga: Manga) {
val currentManga = mangaData.value ?: return val currentManga = delegate.manga.value ?: return
if (currentManga.id != downloadedManga.id) { if (currentManga.id != downloadedManga.id) {
return return
} }
@@ -194,142 +173,16 @@ class DetailsViewModel(
runCatching { runCatching {
localMangaRepository.getDetails(downloadedManga) localMangaRepository.getDetails(downloadedManga)
}.onSuccess { }.onSuccess {
relatedManga.value = it delegate.relatedManga.value = it
}.onFailure { }.onFailure {
if (BuildConfig.DEBUG) { it.printStackTraceDebug()
it.printStackTrace()
}
} }
} }
} }
} }
private fun doLoad() = launchLoadingJob(Dispatchers.Default) { private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
var manga = mangaDataRepository.resolveIntent(intent) delegate.doLoad()
?: 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
} }
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> { private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {

View File

@@ -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
}
}

View File

@@ -12,7 +12,6 @@ import kotlinx.coroutines.sync.Semaphore
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository 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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait 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.referer
import org.koitharu.kotatsu.utils.ext.waitForNetwork import org.koitharu.kotatsu.utils.ext.waitForNetwork
import org.koitharu.kotatsu.utils.progress.ProgressJob import org.koitharu.kotatsu.utils.progress.ProgressJob
@@ -156,9 +156,7 @@ class DownloadManager(
outState.value = DownloadState.Cancelled(startId, manga, cover) outState.value = DownloadState.Cancelled(startId, manga, cover)
throw e throw e
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { e.printStackTraceDebug()
e.printStackTrace()
}
outState.value = DownloadState.Error(startId, manga, cover, e) outState.value = DownloadState.Error(startId, manga, cover, e)
} finally { } finally {
withContext(NonCancellable) { withContext(NonCancellable) {

View File

@@ -3,10 +3,8 @@ package org.koitharu.kotatsu.download.ui
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.flatMapLatest 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.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection import org.koitharu.kotatsu.utils.bindServiceWithLifecycle
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() { class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@@ -28,11 +26,10 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
val adapter = DownloadsAdapter(lifecycleScope, get()) val adapter = DownloadsAdapter(lifecycleScope, get())
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
LifecycleAwareServiceConnection.bindService( bindServiceWithLifecycle(
this, owner = this,
this, service = Intent(this, DownloadService::class.java),
Intent(this, DownloadService::class.java), flags = 0,
0
).service.flatMapLatest { binder -> ).service.flatMapLatest { binder ->
(binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null) (binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null)
}.onEach { }.onEach {

View File

@@ -3,10 +3,11 @@ package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings 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.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -70,9 +71,7 @@ class FavouritesCategoriesViewModel(
return result return result
} }
private fun observeAllCategoriesVisible() = settings.observe() private fun observeAllCategoriesVisible() = settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) {
.filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE } isAllFavouritesVisible
.map { settings.isAllFavouritesVisible } }
.onStart { emit(settings.isAllFavouritesVisible) }
.distinctUntilChanged()
} }

View File

@@ -16,7 +16,7 @@ fun categoryAD(
clickListener.onItemClick(item.category, it) clickListener.onItemClick(item.category, it)
} }
@Suppress("ClickableViewAccessibility") @Suppress("ClickableViewAccessibility")
binding.imageViewHandle.setOnTouchListener { v, event -> binding.imageViewHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) { if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item.category, itemView) clickListener.onItemLongClick(item.category, itemView)
} else { } else {

View File

@@ -2,14 +2,15 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers 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.R
import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode 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.core.ui.DateTimeAgo
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory 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.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst import org.koitharu.kotatsu.utils.ext.onFirst
import java.util.*
import java.util.concurrent.TimeUnit
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
@@ -29,11 +32,7 @@ class HistoryListViewModel(
val isGroupingEnabled = MutableLiveData<Boolean>() val isGroupingEnabled = MutableLiveData<Boolean>()
private val historyGrouping = settings.observe() private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping }
.filter { it == AppSettings.KEY_HISTORY_GROUPING }
.map { settings.historyGrouping }
.onStart { emit(settings.historyGrouping) }
.distinctUntilChanged()
.onEach { isGroupingEnabled.postValue(it) } .onEach { isGroupingEnabled.postValue(it) }
override val content = combine( override val content = combine(

View File

@@ -4,16 +4,14 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode 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.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.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
abstract class MangaListViewModel( abstract class MangaListViewModel(
private val settings: AppSettings, private val settings: AppSettings,
@@ -21,20 +19,15 @@ abstract class MangaListViewModel(
abstract val content: LiveData<List<ListModel>> abstract val content: LiveData<List<ListModel>>
val listMode = MutableLiveData<ListMode>() val listMode = MutableLiveData<ListMode>()
val gridScale = settings.observe() val gridScale = settings.observeAsLiveData(
.filter { it == AppSettings.KEY_GRID_SIZE } context = viewModelScope.coroutineContext + Dispatchers.Default,
.map { settings.gridSize / 100f } key = AppSettings.KEY_GRID_SIZE,
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) { valueProducer = { gridSize / 100f },
settings.gridSize / 100f )
}
open fun onRemoveFilterTag(tag: MangaTag) = Unit open fun onRemoveFilterTag(tag: MangaTag) = Unit
protected fun createListModeFlow() = settings.observe() protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.filter { it == AppSettings.KEY_LIST_MODE }
.map { settings.listMode }
.onStart { emit(settings.listMode) }
.distinctUntilChanged()
.onEach { .onEach {
if (listMode.value != it) { if (listMode.value != it) {
listMode.postValue(it) listMode.postValue(it)

View File

@@ -13,7 +13,7 @@ fun currentFilterAD(
val chipGroup = itemView as ChipsView val chipGroup = itemView as ChipsView
chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data -> chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { _, data ->
listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener) listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener)
} }

View File

@@ -1,18 +1,18 @@
package org.koitharu.kotatsu.list.ui.filter package org.koitharu.kotatsu.list.ui.filter
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import java.util.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.* import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class FilterCoordinator( class FilterCoordinator(
private val repository: RemoteMangaRepository, private val repository: RemoteMangaRepository,
@@ -113,7 +113,7 @@ class FilterCoordinator(
FilterItem.Sort(it, isSelected = it == state.sortOrder) 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)) list.add(FilterItem.Header(R.string.genres, state.tags.size))
tags.mapTo(list) { tags.mapTo(list) {
FilterItem.Tag(it, isChecked = it in state.tags) FilterItem.Tag(it, isChecked = it in state.tags)
@@ -153,9 +153,7 @@ class FilterCoordinator(
runCatching { runCatching {
repository.getTags() repository.getTags()
}.onFailure { error -> }.onFailure { error ->
if (BuildConfig.DEBUG) { error.printStackTraceDebug()
error.printStackTrace()
}
}.getOrNull() }.getOrNull()
} }

View File

@@ -15,11 +15,11 @@ import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmSuppressWildcards Uri>> { class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmSuppressWildcards Uri>> {
@@ -68,9 +68,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
try { try {
importCall.launch(arrayOf("*/*")) importCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) { e.printStackTraceDebug()
e.printStackTrace()
}
Snackbar.make( Snackbar.make(
binding.recyclerView, binding.recyclerView,
R.string.operation_not_supported, R.string.operation_not_supported,

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.ui
import android.net.Uri import android.net.Uri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -10,7 +11,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings 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.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
import java.io.IOException
class LocalListViewModel( class LocalListViewModel(
private val repository: LocalMangaRepository, private val repository: LocalMangaRepository,
@@ -127,9 +127,7 @@ class LocalListViewModel(
runCatching { runCatching {
repository.cleanup() repository.cleanup()
}.onFailure { error -> }.onFailure { error ->
if (BuildConfig.DEBUG) { error.printStackTraceDebug()
error.printStackTrace()
}
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.prefs.AppSettings 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.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
@@ -21,11 +22,11 @@ class MainViewModel(
val onOpenReader = SingleLiveEvent<Manga>() val onOpenReader = SingleLiveEvent<Manga>()
var defaultSection by settings::defaultSection var defaultSection by settings::defaultSection
val isSuggestionsEnabled = settings.observe() val isSuggestionsEnabled = settings.observeAsLiveData(
.filter { it == AppSettings.KEY_SUGGESTIONS } context = viewModelScope.coroutineContext + Dispatchers.Default,
.onStart { emit("") } key = AppSettings.KEY_SUGGESTIONS,
.map { settings.isSuggestionsEnabled } valueProducer = { isSuggestionsEnabled }
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) )
val isResumeEnabled = historyRepository val isResumeEnabled = historyRepository
.observeHasItems() .observeHasItems()

View File

@@ -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,
)

View File

@@ -6,11 +6,13 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.* import androidx.core.view.OnApplyWindowInsetsListener
import androidx.fragment.app.commit 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.lifecycle.lifecycleScope
import androidx.transition.Slide import androidx.transition.Slide
import androidx.transition.TransitionManager 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage 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.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.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity 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.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
import org.koitharu.kotatsu.utils.ext.observeWithPrevious import org.koitharu.kotatsu.utils.ext.observeWithPrevious
import org.koitharu.kotatsu.utils.ext.postDelayed
import java.util.concurrent.TimeUnit
class ReaderActivity : class ReaderActivity :
BaseFullscreenActivity<ActivityReaderBinding>(), BaseFullscreenActivity<ActivityReaderBinding>(),
@@ -75,13 +75,13 @@ class ReaderActivity :
private lateinit var controlDelegate: ReaderControlDelegate private lateinit var controlDelegate: ReaderControlDelegate
private val savePageRequest = registerForActivityResult(PageSaveContract(), this) private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var gestureInsets: Insets = Insets.NONE private var gestureInsets: Insets = Insets.NONE
private lateinit var readerManager: ReaderManager
private val reader private val hideUiRunnable = Runnable { setUiIsVisible(false) }
get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReader<*>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityReaderBinding.inflate(layoutInflater)) setContentView(ActivityReaderBinding.inflate(layoutInflater))
readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this) touchHelper = GridTouchHelper(this, this)
orientationHelper = ScreenOrientationHelper(this) orientationHelper = ScreenOrientationHelper(this)
@@ -91,6 +91,7 @@ class ReaderActivity :
insetsDelegate.interceptingWindowInsetsListener = this insetsDelegate.interceptingWindowInsetsListener = this
orientationHelper.observeAutoOrientation() orientationHelper.observeAutoOrientation()
.flowWithLifecycle(lifecycle)
.onEach { .onEach {
binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
@@ -113,33 +114,20 @@ class ReaderActivity :
} }
private fun onInitReader(mode: ReaderMode) { private fun onInitReader(mode: ReaderMode) {
val currentReader = reader if (readerManager.currentMode != mode) {
when (mode) { readerManager.replace(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())
}
}
} }
binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).setIcon( val iconRes = when (mode) {
when (mode) { ReaderMode.WEBTOON -> R.drawable.ic_script
ReaderMode.WEBTOON -> R.drawable.ic_script ReaderMode.REVERSED -> R.drawable.ic_read_reversed
ReaderMode.REVERSED -> R.drawable.ic_read_reversed ReaderMode.STANDARD -> R.drawable.ic_book_page
ReaderMode.STANDARD -> R.drawable.ic_book_page }
} binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).run {
) setIcon(iconRes)
binding.appbarTop.postDelayed(1000) { setVisible(true)
setUiIsVisible(false) }
if (binding.appbarTop.isVisible) {
lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
} }
} }
@@ -151,18 +139,8 @@ class ReaderActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_reader_mode -> { R.id.action_reader_mode -> {
ReaderConfigDialog.show( val currentMode = readerManager.currentMode ?: return false
supportFragmentManager, ReaderConfigDialog.show(supportFragmentManager, currentMode)
when (reader) {
is PagerReaderFragment -> ReaderMode.STANDARD
is WebtoonReaderFragment -> ReaderMode.WEBTOON
is ReversedReaderFragment -> ReaderMode.REVERSED
else -> {
showWaitWhileLoading()
return false
}
}
)
} }
R.id.action_settings -> { R.id.action_settings -> {
startActivity(SettingsActivity.newReaderSettingsIntent(this)) startActivity(SettingsActivity.newReaderSettingsIntent(this))
@@ -184,17 +162,17 @@ class ReaderActivity :
supportFragmentManager, supportFragmentManager,
pages, pages,
title?.toString().orEmpty(), title?.toString().orEmpty(),
reader?.getCurrentState()?.page ?: -1 readerManager.currentReader?.getCurrentState()?.page ?: -1,
) )
} else { } else {
showWaitWhileLoading() return false
} }
} }
R.id.action_save_page -> { R.id.action_save_page -> {
viewModel.getCurrentPage()?.also { page -> viewModel.getCurrentPage()?.also { page ->
viewModel.saveCurrentState(reader?.getCurrentState()) viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.saveCurrentPage(page, savePageRequest) viewModel.saveCurrentPage(page, savePageRequest)
} ?: showWaitWhileLoading() } ?: return false
} }
R.id.action_bookmark -> { R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value == true) { if (viewModel.isBookmarkAdded.value == true) {
@@ -216,10 +194,14 @@ class ReaderActivity :
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty() val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
binding.layoutLoading.isVisible = isLoading && !hasPages binding.layoutLoading.isVisible = isLoading && !hasPages
if (isLoading && hasPages) { if (isLoading && hasPages) {
binding.toastView.show(R.string.loading_, true) binding.toastView.show(R.string.loading_)
} else { } else {
binding.toastView.hide() 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) { private fun onError(e: Throwable) {
@@ -279,14 +261,14 @@ class ReaderActivity :
val index = pages.indexOfFirst { it.id == page.id } val index = pages.indexOfFirst { it.id == page.id }
if (index != -1) { if (index != -1) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
reader?.switchPageTo(index, true) readerManager.currentReader?.switchPageTo(index, true)
} }
} }
} }
} }
override fun onReaderModeChanged(mode: ReaderMode) { override fun onReaderModeChanged(mode: ReaderMode) {
viewModel.saveCurrentState(reader?.getCurrentState()) viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.switchMode(mode) 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) { private fun setWindowSecure(isSecure: Boolean) {
if (isSecure) { if (isSecure) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@@ -358,7 +334,7 @@ class ReaderActivity :
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun switchPageBy(delta: Int) { override fun switchPageBy(delta: Int) {
reader?.switchPageBy(delta) readerManager.currentReader?.switchPageBy(delta)
} }
override fun toggleUiVisibility() { override fun toggleUiVisibility() {

View File

@@ -5,14 +5,16 @@ import android.view.SoundEffectConstants
import android.view.View import android.view.View
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.Dispatchers 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.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.utils.GridTouchHelper import org.koitharu.kotatsu.utils.GridTouchHelper
@Suppress("UNUSED_PARAMETER")
class ReaderControlDelegate( class ReaderControlDelegate(
private val scope: LifecycleCoroutineScope, scope: LifecycleCoroutineScope,
private val settings: AppSettings, settings: AppSettings,
private val listener: OnInteractionListener private val listener: OnInteractionListener
) { ) {
@@ -20,12 +22,8 @@ class ReaderControlDelegate(
private var isVolumeKeysSwitchEnabled: Boolean = false private var isVolumeKeysSwitchEnabled: Boolean = false
init { init {
settings.observe() settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch }
.filter { it == AppSettings.KEY_READER_SWITCHERS } .flowOn(Dispatchers.Default)
.map { settings.readerPageSwitch }
.onStart { emit(settings.readerPageSwitch) }
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.onEach { .onEach {
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS 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) { KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
listener.switchPageBy(-1) listener.switchPageBy(-1)
true true
@@ -92,9 +90,11 @@ class ReaderControlDelegate(
else -> false else -> false
} }
fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean {
return (isVolumeKeysSwitchEnabled && return (
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)) isVolumeKeysSwitchEnabled &&
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)
)
} }
interface OnInteractionListener { interface OnInteractionListener {

View File

@@ -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) }
}
}

View File

@@ -1,13 +1,11 @@
package org.koitharu.kotatsu.reader.ui package org.koitharu.kotatsu.reader.ui
import android.content.Context import android.content.Context
import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.transition.Fade import androidx.transition.Fade
import androidx.transition.Slide import androidx.transition.Slide
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
@@ -15,26 +13,28 @@ import androidx.transition.TransitionSet
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
class ReaderToastView @JvmOverloads constructor( class ReaderToastView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : MaterialTextView(context, attrs, defStyleAttr) { ) : MaterialTextView(context, attrs, defStyleAttr) {
private var hideRunnable = Runnable { private var hideRunnable = Runnable {
hide() hide()
} }
fun show(message: CharSequence, isLoading: Boolean) { fun show(message: CharSequence) {
removeCallbacks(hideRunnable) removeCallbacks(hideRunnable)
text = message text = message
setupTransition() setupTransition()
isVisible = true isVisible = true
} }
fun show(@StringRes messageId: Int, isLoading: Boolean) { fun show(@StringRes messageId: Int) {
show(context.getString(messageId), isLoading) show(context.getString(messageId))
} }
fun showTemporary(message: CharSequence, duration: Long) { fun showTemporary(message: CharSequence, duration: Long) {
show(message, false) show(message)
postDelayed(hideRunnable, duration) postDelayed(hideRunnable, duration)
} }
@@ -49,7 +49,7 @@ class ReaderToastView @JvmOverloads constructor(
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
private fun setupTransition () { private fun setupTransition() {
val parentView = parent as? ViewGroup ?: return val parentView = parent as? ViewGroup ?: return
val transition = TransitionSet() val transition = TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER) .setOrdering(TransitionSet.ORDERING_TOGETHER)
@@ -58,14 +58,4 @@ class ReaderToastView @JvmOverloads constructor(
.addTransition(Fade()) .addTransition(Fade())
TransitionManager.beginDelayedTransition(parentView, transition) 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
}
} }

View File

@@ -8,9 +8,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* 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.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent 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.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.*
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage 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.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.SingleLiveEvent 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.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import java.util.* 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( class ReaderViewModel(
private val intent: MangaIntent, private val intent: MangaIntent,
initialState: ReaderState?, initialState: ReaderState?,
@@ -78,22 +78,19 @@ class ReaderViewModel(
val manga: Manga? val manga: Manga?
get() = mangaData.value get() = mangaData.value
val readerAnimation = settings.observe() val readerAnimation = settings.observeAsLiveData(
.filter { it == AppSettings.KEY_READER_ANIMATION } context = viewModelScope.coroutineContext + Dispatchers.Default,
.map { settings.readerAnimation } key = AppSettings.KEY_READER_ANIMATION,
.onStart { emit(settings.readerAnimation) } valueProducer = { readerAnimation }
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) )
val isScreenshotsBlockEnabled = combine( val isScreenshotsBlockEnabled = combine(
mangaData, mangaData,
settings.observe() settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
.filter { it == AppSettings.KEY_SCREENSHOTS_POLICY }
.onStart { emit("") }
.map { settings.screenshotsPolicy },
) { manga, policy -> ) { manga, policy ->
policy == ScreenshotsPolicy.BLOCK_ALL || policy == ScreenshotsPolicy.BLOCK_ALL ||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val onZoomChanged = SingleLiveEvent<Unit>() val onZoomChanged = SingleLiveEvent<Unit>()
@@ -142,7 +139,7 @@ class ReaderViewModel(
if (state != null) { if (state != null) {
currentState.value = state currentState.value = state
} }
saveState( historyRepository.saveStateAsync(
mangaData.value ?: return, mangaData.value ?: return,
state ?: currentState.value ?: return state ?: currentState.value ?: return
) )
@@ -169,9 +166,7 @@ class ReaderViewModel(
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
if (BuildConfig.DEBUG) { e.printStackTraceDebug()
e.printStackTrace()
}
onPageSaved.postCall(null) onPageSaved.postCall(null)
} }
} }
@@ -286,7 +281,7 @@ class ReaderViewModel(
} }
val branch = chapters[currentState.value?.chapterId ?: 0L].branch 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) readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId) val pages = loadChapter(requireNotNull(currentState.value).chapterId)
@@ -349,9 +344,9 @@ class ReaderViewModel(
private fun subscribeToSettings() { private fun subscribeToSettings() {
settings.observe() settings.observe()
.filter { it == AppSettings.KEY_ZOOM_MODE } .onEach { key ->
.onEach { onZoomChanged.postCall(Unit) } if (key == AppSettings.KEY_ZOOM_MODE) onZoomChanged.postCall(Unit)
.launchIn(viewModelScope + Dispatchers.IO) }.launchIn(viewModelScope + Dispatchers.Default)
} }
private fun <T> List<T>.trySublist(fromIndex: Int, toIndex: Int): List<T> { private fun <T> List<T>.trySublist(fromIndex: Int, toIndex: Int): List<T> {
@@ -363,40 +358,23 @@ class ReaderViewModel(
subList(fromIndexBounded, toIndexBounded) subList(fromIndexBounded, toIndexBounded)
} }
} }
}
private fun Manga.copy(chapters: List<MangaChapter>?) = Manga( /**
id = id, * This function is not a member of the ReaderViewModel
title = title, * because it should work independently of the ViewModel's lifecycle.
altTitle = altTitle, */
url = url, private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState): Job {
publicUrl = publicUrl, return processLifecycleScope.launch(Dispatchers.Default) {
rating = rating, runCatching {
isNsfw = isNsfw, addOrUpdate(
coverUrl = coverUrl, manga = manga,
tags = tags, chapterId = state.chapterId,
state = state, page = state.page,
author = author, scroll = state.scroll
largeCoverUrl = largeCoverUrl, )
description = description, }.onFailure {
chapters = chapters, it.printStackTraceDebug()
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
)
}
} }
} }
} }

View File

@@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
private const val FILTER_MIN_INTERVAL = 750L private const val FILTER_MIN_INTERVAL = 750L
@@ -133,9 +133,7 @@ class RemoteListViewModel(
} }
hasNextPage.value = list.isNotEmpty() hasNextPage.value = list.isNotEmpty()
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { e.printStackTraceDebug()
e.printStackTrace()
}
listError.value = e listError.value = e
if (!mangaList.value.isNullOrEmpty()) { if (!mangaList.value.isNullOrEmpty()) {
onError.postCall(e) onError.postCall(e)
@@ -158,4 +156,4 @@ class RemoteListViewModel(
textSecondary = 0, textSecondary = 0,
actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter, actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter,
) )
} }

View File

@@ -8,15 +8,6 @@ import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.MainThread import androidx.annotation.MainThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get 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.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.utils.FileSize 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) { class AppUpdateChecker(private val activity: ComponentActivity) {
@@ -45,8 +46,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
suspend fun checkNow() = runCatching { suspend fun checkNow() = runCatching {
val version = repo.getLatestVersion() val version = repo.getLatestVersion()
val newVersionId = VersionId.parse(version.name) val newVersionId = VersionId(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME) val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
val result = newVersionId > currentVersionId val result = newVersionId > currentVersionId
if (result) { if (result) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -56,7 +57,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
settings.lastUpdateCheckTimestamp = System.currentTimeMillis() settings.lastUpdateCheckTimestamp = System.currentTimeMillis()
result result
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTraceDebug()
}.getOrNull() }.getOrNull()
@MainThread @MainThread
@@ -99,7 +100,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
PackageManager.GET_SIGNATURES PackageManager.GET_SIGNATURES
) )
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace() e.printStackTraceDebug()
return null return null
} }
val signatures = packageInfo?.signatures val signatures = packageInfo?.signatures
@@ -109,7 +110,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
val cf = CertificateFactory.getInstance("X509") val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) { } catch (e: CertificateException) {
e.printStackTrace() e.printStackTraceDebug()
return null return null
} }
return try { return try {
@@ -117,10 +118,10 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
val publicKey: ByteArray = md.digest(c.encoded) val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted() publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
e.printStackTrace() e.printStackTraceDebug()
null null
} catch (e: CertificateEncodingException) { } catch (e: CertificateEncodingException) {
e.printStackTrace() e.printStackTraceDebug()
null null
} }
} }

View File

@@ -6,7 +6,6 @@ import androidx.preference.Preference
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.parser.MangaRepository 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.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity 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.serializableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@@ -70,9 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
preference.title = getString(R.string.logged_in_as, username) preference.title = getString(R.string.logged_in_as, username)
}.onFailure { error -> }.onFailure { error ->
preference.isEnabled = error is AuthRequiredException preference.isEnabled = error is AuthRequiredException
if (BuildConfig.DEBUG) { error.printStackTraceDebug()
error.printStackTrace()
}
} }
} }

View File

@@ -7,12 +7,13 @@ import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings 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?> { ActivityResultCallback<Uri?> {
private val backupSelectCall = registerForActivityResult( private val backupSelectCall = registerForActivityResult(
@@ -34,9 +35,7 @@ class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
try { try {
backupSelectCall.launch(arrayOf("*/*")) backupSelectCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) { e.printStackTraceDebug()
e.printStackTrace()
}
Snackbar.make( Snackbar.make(
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT
).show() ).show()

View File

@@ -10,7 +10,7 @@ import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
class LifecycleAwareServiceConnection private constructor( class LifecycleAwareServiceConnection(
private val host: Activity, private val host: Activity,
) : ServiceConnection, DefaultLifecycleObserver { ) : ServiceConnection, DefaultLifecycleObserver {
@@ -31,19 +31,15 @@ class LifecycleAwareServiceConnection private constructor(
super.onDestroy(owner) super.onDestroy(owner)
host.unbindService(this) host.unbindService(this)
} }
}
companion object { fun Activity.bindServiceWithLifecycle(
owner: LifecycleOwner,
fun bindService( service: Intent,
host: Activity, flags: Int
lifecycleOwner: LifecycleOwner, ): LifecycleAwareServiceConnection {
service: Intent, val connection = LifecycleAwareServiceConnection(this)
flags: Int, bindService(service, connection, flags)
): LifecycleAwareServiceConnection { owner.lifecycle.addObserver(connection)
val connection = LifecycleAwareServiceConnection(host) return connection
host.bindService(service, connection, flags)
lifecycleOwner.lifecycle.addObserver(connection)
return connection
}
}
} }

View File

@@ -9,7 +9,11 @@ import android.net.Uri
import android.os.Build import android.os.Build
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
@@ -55,4 +59,11 @@ fun <I> ActivityResultLauncher<I>.tryLaunch(input: I, options: ActivityOptionsCo
return runCatching { return runCatching {
launch(input, options) launch(input, options)
}.isSuccess }.isSuccess
}
fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
coroutineScope.launch {
delay(delay)
runnable.run()
}
} }

View File

@@ -3,15 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope 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 val processLifecycleScope: LifecycleCoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope inline get() = ProcessLifecycleOwner.get().lifecycleScope

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import kotlinx.coroutines.Deferred
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@@ -9,12 +9,14 @@
android:id="@+id/action_bookmark" android:id="@+id/action_bookmark"
android:icon="@drawable/ic_bookmark" android:icon="@drawable/ic_bookmark"
android:title="@string/bookmark_add" android:title="@string/bookmark_add"
android:visible="false"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
android:id="@+id/action_pages_thumbs" android:id="@+id/action_pages_thumbs"
android:icon="@drawable/ic_grid" android:icon="@drawable/ic_grid"
android:title="@string/pages" android:title="@string/pages"
android:visible="false"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
@@ -28,11 +30,13 @@
android:id="@+id/action_reader_mode" android:id="@+id/action_reader_mode"
android:icon="@drawable/ic_loading" android:icon="@drawable/ic_loading"
android:title="@string/read_mode" android:title="@string/read_mode"
android:visible="false"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
android:id="@+id/action_save_page" android:id="@+id/action_save_page"
android:title="@string/save_page" android:title="@string/save_page"
android:visible="false"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item

View File

@@ -0,0 +1,5 @@
package org.koitharu.kotatsu.utils.ext
import org.koitharu.kotatsu.BuildConfig
inline fun Throwable.printStackTraceDebug() = Unit