Refactor and optimization
This commit is contained in:
3
.idea/inspectionProfiles/Project_Default.xml
generated
3
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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" />
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||||
@@ -3,21 +3,21 @@ package org.koitharu.kotatsu.base.domain
|
|||||||
import android.graphics.BitmapFactory
|
import android.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,5 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val githubModule
|
val githubModule
|
||||||
get() = module {
|
get() = module {
|
||||||
factory {
|
factory { GithubRepository(get()) }
|
||||||
GithubRepository(get())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
|
import androidx.lifecycle.liveData
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
|
||||||
|
var lastValue: T = valueProducer()
|
||||||
|
emit(lastValue)
|
||||||
|
observe().collect {
|
||||||
|
if (it == key) {
|
||||||
|
val value = valueProducer()
|
||||||
|
if (value != lastValue) {
|
||||||
|
emit(value)
|
||||||
|
}
|
||||||
|
lastValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> AppSettings.observeAsLiveData(
|
||||||
|
context: CoroutineContext,
|
||||||
|
key: String,
|
||||||
|
valueProducer: AppSettings.() -> T
|
||||||
|
) = liveData(context) {
|
||||||
|
emit(valueProducer())
|
||||||
|
observe().collect {
|
||||||
|
if (it == key) {
|
||||||
|
val value = valueProducer()
|
||||||
|
if (value != latestValue) {
|
||||||
|
emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
|
|||||||
|
|
||||||
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package org.koitharu.kotatsu.details.ui
|
||||||
|
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
|
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import org.koitharu.kotatsu.utils.ext.iterator
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
|
class MangaDetailsDelegate(
|
||||||
|
private val intent: MangaIntent,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val mangaData = MutableStateFlow(intent.manga)
|
||||||
|
|
||||||
|
val selectedBranch = MutableStateFlow<String?>(null)
|
||||||
|
// Remote manga for saved and saved for remote
|
||||||
|
val relatedManga = MutableStateFlow<Manga?>(null)
|
||||||
|
val manga: StateFlow<Manga?>
|
||||||
|
get() = mangaData
|
||||||
|
val mangaId = intent.manga?.id ?: intent.mangaId
|
||||||
|
|
||||||
|
suspend fun doLoad() {
|
||||||
|
var manga = mangaDataRepository.resolveIntent(intent)
|
||||||
|
?: throw MangaNotFoundException("Cannot find manga")
|
||||||
|
mangaData.value = manga
|
||||||
|
manga = MangaRepository(manga.source).getDetails(manga)
|
||||||
|
// find default branch
|
||||||
|
val hist = historyRepository.getOne(manga)
|
||||||
|
selectedBranch.value = if (hist != null) {
|
||||||
|
val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
|
||||||
|
if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
|
||||||
|
} else {
|
||||||
|
predictBranch(manga.chapters)
|
||||||
|
}
|
||||||
|
mangaData.value = manga
|
||||||
|
relatedManga.value = runCatching {
|
||||||
|
if (manga.source == MangaSource.LOCAL) {
|
||||||
|
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
|
||||||
|
MangaRepository(m.source).getDetails(m)
|
||||||
|
} else {
|
||||||
|
localMangaRepository.findSavedManga(manga)
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
error.printStackTraceDebug()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapChapters(
|
||||||
|
manga: Manga?,
|
||||||
|
related: Manga?,
|
||||||
|
history: MangaHistory?,
|
||||||
|
newCount: Int,
|
||||||
|
branch: String?,
|
||||||
|
): List<ChapterListItem> {
|
||||||
|
val chapters = manga?.chapters ?: return emptyList()
|
||||||
|
val relatedChapters = related?.chapters
|
||||||
|
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
||||||
|
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
||||||
|
} else {
|
||||||
|
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapChapters(
|
||||||
|
chapters: List<MangaChapter>,
|
||||||
|
downloadedChapters: List<MangaChapter>?,
|
||||||
|
currentId: Long?,
|
||||||
|
newCount: Int,
|
||||||
|
branch: String?,
|
||||||
|
): List<ChapterListItem> {
|
||||||
|
val result = ArrayList<ChapterListItem>(chapters.size)
|
||||||
|
val dateFormat = settings.getDateFormat()
|
||||||
|
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
||||||
|
val firstNewIndex = chapters.size - newCount
|
||||||
|
val downloadedIds = downloadedChapters?.mapToSet { it.id }
|
||||||
|
for (i in chapters.indices) {
|
||||||
|
val chapter = chapters[i]
|
||||||
|
if (chapter.branch != branch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result += chapter.toListItem(
|
||||||
|
isCurrent = i == currentIndex,
|
||||||
|
isUnread = i > currentIndex,
|
||||||
|
isNew = i >= firstNewIndex,
|
||||||
|
isMissing = false,
|
||||||
|
isDownloaded = downloadedIds?.contains(chapter.id) == true,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapChaptersWithSource(
|
||||||
|
chapters: List<MangaChapter>,
|
||||||
|
sourceChapters: List<MangaChapter>,
|
||||||
|
currentId: Long?,
|
||||||
|
newCount: Int,
|
||||||
|
branch: String?,
|
||||||
|
): List<ChapterListItem> {
|
||||||
|
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
||||||
|
val result = ArrayList<ChapterListItem>(sourceChapters.size)
|
||||||
|
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
|
||||||
|
val firstNewIndex = sourceChapters.size - newCount
|
||||||
|
val dateFormat = settings.getDateFormat()
|
||||||
|
for (i in sourceChapters.indices) {
|
||||||
|
val chapter = sourceChapters[i]
|
||||||
|
val localChapter = chaptersMap.remove(chapter.id)
|
||||||
|
if (chapter.branch != branch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result += localChapter?.toListItem(
|
||||||
|
isCurrent = i == currentIndex,
|
||||||
|
isUnread = i > currentIndex,
|
||||||
|
isNew = i >= firstNewIndex,
|
||||||
|
isMissing = false,
|
||||||
|
isDownloaded = false,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
) ?: chapter.toListItem(
|
||||||
|
isCurrent = i == currentIndex,
|
||||||
|
isUnread = i > currentIndex,
|
||||||
|
isNew = i >= firstNewIndex,
|
||||||
|
isMissing = true,
|
||||||
|
isDownloaded = false,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
|
||||||
|
result.ensureCapacity(result.size + chaptersMap.size)
|
||||||
|
chaptersMap.values.mapNotNullTo(result) {
|
||||||
|
if (it.branch == branch) {
|
||||||
|
it.toListItem(
|
||||||
|
isCurrent = false,
|
||||||
|
isUnread = true,
|
||||||
|
isNew = false,
|
||||||
|
isMissing = false,
|
||||||
|
isDownloaded = false,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sortBy { it.chapter.number }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun predictBranch(chapters: List<MangaChapter>?): String? {
|
||||||
|
if (chapters.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val groups = chapters.groupBy { it.branch }
|
||||||
|
for (locale in LocaleListCompat.getAdjustedDefault()) {
|
||||||
|
var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
|
||||||
|
if (groups.containsKey(language)) {
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
language = locale.getDisplayName(locale).toTitleCase(locale)
|
||||||
|
if (groups.containsKey(language)) {
|
||||||
|
return language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups.maxByOrNull { it.value.size }?.key
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.sync.Semaphore
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.data
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
|
||||||
|
fun Manga.filterChapters(branch: String?): Manga {
|
||||||
|
if (chapters.isNullOrEmpty()) return this
|
||||||
|
return copy(chapters = chapters?.filter { it.branch == branch })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Manga.copy(chapters: List<MangaChapter>?) = Manga(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
altTitle = altTitle,
|
||||||
|
url = url,
|
||||||
|
publicUrl = publicUrl,
|
||||||
|
rating = rating,
|
||||||
|
isNsfw = isNsfw,
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
tags = tags,
|
||||||
|
state = state,
|
||||||
|
author = author,
|
||||||
|
largeCoverUrl = largeCoverUrl,
|
||||||
|
description = description,
|
||||||
|
chapters = chapters,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
@@ -6,11 +6,13 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ReaderManager(
|
||||||
|
private val fragmentManager: FragmentManager,
|
||||||
|
@IdRes private val containerResId: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val modeMap = EnumMap<ReaderMode, Class<out BaseReader<*>>>(ReaderMode::class.java)
|
||||||
|
|
||||||
|
init {
|
||||||
|
modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java
|
||||||
|
modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java
|
||||||
|
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentReader: BaseReader<*>?
|
||||||
|
get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*>
|
||||||
|
|
||||||
|
val currentMode: ReaderMode?
|
||||||
|
get() {
|
||||||
|
val readerClass = currentReader?.javaClass ?: return null
|
||||||
|
return modeMap.entries.find { it.value == readerClass }?.key
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replace(newMode: ReaderMode) {
|
||||||
|
val readerClass = requireNotNull(modeMap[newMode])
|
||||||
|
fragmentManager.commit {
|
||||||
|
replace(containerResId, readerClass, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replace(reader: BaseReader<*>) {
|
||||||
|
fragmentManager.commit { replace(containerResId, reader) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
|
||||||
|
inline fun Throwable.printStackTraceDebug() = Unit
|
||||||
Reference in New Issue
Block a user