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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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.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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.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() {

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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