Replace LiveData with StateFlow

This commit is contained in:
Koitharu
2023-05-27 12:25:49 +03:00
parent 47f346b42c
commit 5a0c54e00f
147 changed files with 1047 additions and 1039 deletions

View File

@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
import javax.inject.Inject

View File

@@ -30,6 +30,8 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.reverseAsync
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
@@ -81,8 +83,8 @@ class BookmarksFragment :
binding.recyclerView.addItemDecoration(spacingDecoration)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone)
}
override fun onDestroyView() {

View File

@@ -1,18 +1,21 @@
package org.koitharu.kotatsu.bookmarks.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
@@ -25,9 +28,9 @@ class BookmarksViewModel @Inject constructor(
private val repository: BookmarksRepository,
) : BaseViewModel() {
val onActionDone = SingleLiveEvent<ReversibleAction>()
val onActionDone = MutableEventFlow<ReversibleAction>()
val content: LiveData<List<ListModel>> = repository.observeBookmarks()
val content: StateFlow<List<ListModel>> = repository.observeBookmarks()
.map { list ->
if (list.isEmpty()) {
listOf(
@@ -43,12 +46,12 @@ class BookmarksViewModel @Inject constructor(
}
}
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
launchJob(Dispatchers.Default) {
val handle = repository.removeBookmarks(ids)
onActionDone.emitCall(ReversibleAction(R.string.bookmarks_removed, handle))
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
}
}
}

View File

@@ -42,9 +42,9 @@ import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher

View File

@@ -22,10 +22,7 @@ class DialogErrorObserver(
fragment: Fragment?,
) : this(host, fragment, null, null)
override fun onChanged(value: Throwable?) {
if (value == null) {
return
}
override suspend fun emit(value: Throwable) {
val listener = DialogListener(value)
val dialogBuilder = MaterialAlertDialogBuilder(activity ?: host.context)
.setMessage(value.getDisplayMessage(host.context.resources))

View File

@@ -7,8 +7,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.util.ext.findActivity
@@ -19,7 +19,7 @@ abstract class ErrorObserver(
protected val fragment: Fragment?,
private val resolver: ExceptionResolver?,
private val onResolved: Consumer<Boolean>?,
) : Observer<Throwable?> {
) : FlowCollector<Throwable> {
protected val activity = host.context.findActivity()

View File

@@ -22,10 +22,7 @@ class SnackbarErrorObserver(
fragment: Fragment?,
) : this(host, fragment, null, null)
override fun onChanged(value: Throwable?) {
if (value == null) {
return
}
override suspend fun emit(value: Throwable) {
val snackbar = Snackbar.make(host, value.getDisplayMessage(host.context.resources), Snackbar.LENGTH_SHORT)
if (activity is BottomNavOwner) {
snackbar.anchorView = activity.bottomNav

View File

@@ -24,6 +24,10 @@ fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
return acc.values.max()
}
fun Manga.findChapter(id: Long): MangaChapter? {
return chapters?.find { it.id == id }
}
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
val ch = chapters
if (ch.isNullOrEmpty()) {

View File

@@ -27,7 +27,7 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity

View File

@@ -1,37 +1,24 @@
package org.koitharu.kotatsu.core.parser
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject
import kotlin.math.roundToInt
@Reusable
class MangaDataRepository @Inject constructor(
@@ -104,67 +91,10 @@ class MangaDataRepository @Inject constructor(
}
}
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
suspend fun determineMangaIsWebtoon(repository: MangaRepository, pages: List<MangaPage>): Boolean {
val pageIndex = (pages.size * 0.3).roundToInt()
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = Request.Builder()
.url(url)
.get()
.tag(MangaSource::class.java, page.source)
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
.build()
okHttpClient.newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * MIN_WEBTOON_RATIO < size.height
}
private fun newEntity(mangaId: Long) = MangaPrefsEntity(
mangaId = mangaId,
mode = -1,
cfBrightness = 0f,
cfContrast = 0f,
)
companion object {
private const val MIN_WEBTOON_RATIO = 2
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.parser
import androidx.annotation.AnyThread
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter

View File

@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
import java.io.File
import java.net.Proxy
import java.util.Collections

View File

@@ -1,13 +1,11 @@
package org.koitharu.kotatsu.core.prefs
import androidx.lifecycle.liveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
import kotlin.coroutines.CoroutineContext
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
var lastValue: T = valueProducer()
@@ -23,25 +21,9 @@ fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() ->
}
}
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)
}
}
}
}
fun <T> AppSettings.observeAsStateFlow(
key: String,
scope: CoroutineScope,
key: String,
valueProducer: AppSettings.() -> T,
): StateFlow<T> = observe().transform {
if (it == key) {

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
@@ -8,9 +7,16 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.ui.util.CountedBooleanLiveData
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.ext.EventFlow
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -18,16 +24,17 @@ import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseViewModel : ViewModel() {
@JvmField
protected val loadingCounter = CountedBooleanLiveData()
protected val loadingCounter = MutableStateFlow(0)
@JvmField
protected val errorEvent = SingleLiveEvent<Throwable>()
protected val errorEvent = MutableEventFlow<Throwable>()
val onError: LiveData<Throwable>
val onError: EventFlow<Throwable>
get() = errorEvent
val isLoading: LiveData<Boolean>
get() = loadingCounter
val isLoading: StateFlow<Boolean>
get() = loadingCounter.map { it > 0 }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
@@ -51,7 +58,11 @@ abstract class BaseViewModel : ViewModel() {
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
errorEvent.postCall(throwable)
errorEvent.call(throwable)
}
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
}

View File

@@ -1,31 +0,0 @@
package org.koitharu.kotatsu.core.ui.util
import androidx.annotation.AnyThread
import androidx.lifecycle.LiveData
import java.util.concurrent.atomic.AtomicInteger
class CountedBooleanLiveData : LiveData<Boolean>(false) {
private val counter = AtomicInteger(0)
@AnyThread
fun increment() {
if (counter.getAndIncrement() == 0) {
postValue(true)
}
}
@AnyThread
fun decrement() {
if (counter.decrementAndGet() == 0) {
postValue(false)
}
}
@AnyThread
fun reset() {
if (counter.getAndSet(0) != 0) {
postValue(false)
}
}
}

View File

@@ -1,18 +1,15 @@
package org.koitharu.kotatsu.core.ui.util
import android.view.View
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.R
class ReversibleActionObserver(
private val snackbarHost: View,
) : Observer<ReversibleAction?> {
) : FlowCollector<ReversibleAction> {
override fun onChanged(value: ReversibleAction?) {
if (value == null) {
return
}
override suspend fun emit(value: ReversibleAction) {
val handle = value.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(snackbarHost, value.stringResId, length)

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.core.util
import kotlinx.coroutines.flow.FlowCollector
class Event<T>(
private val data: T,
) {
private var isConsumed = false
suspend fun consume(collector: FlowCollector<T>) {
if (isConsumed) {
collector.emit(data)
isConsumed = true
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Event<*>
if (data != other.data) return false
return isConsumed == other.isConsumed
}
override fun hashCode(): Int {
var result = data?.hashCode() ?: 0
result = 31 * result + isConsumed.hashCode()
return result
}
override fun toString(): String {
return "Event(data=$data, isConsumed=$isConsumed)"
}
}

View File

@@ -1,86 +0,0 @@
package org.koitharu.kotatsu.core.util
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private const val DEFAULT_TIMEOUT = 5_000L
/**
* Similar to a CoroutineLiveData but optimized for using within infinite flows
*/
class FlowLiveData<T>(
private val flow: Flow<T>,
defaultValue: T,
context: CoroutineContext = EmptyCoroutineContext,
private val timeoutInMs: Long = DEFAULT_TIMEOUT,
) : LiveData<T>(defaultValue) {
private val scope = CoroutineScope(Dispatchers.Main.immediate + context + SupervisorJob(context[Job]))
private var job: Job? = null
private var cancellationJob: Job? = null
override fun onActive() {
super.onActive()
cancellationJob?.cancel()
cancellationJob = null
if (job?.isActive == true) {
return
}
job = scope.launch {
flow.collect(Collector())
}
}
override fun onInactive() {
super.onInactive()
cancellationJob?.cancel()
cancellationJob = scope.launch(Dispatchers.Main.immediate) {
delay(timeoutInMs)
if (!hasActiveObservers()) {
job?.cancel()
job = null
}
}
}
private inner class Collector : FlowCollector<T> {
private var previousValue: Any? = value
private val dispatcher = Dispatchers.Main.immediate
override suspend fun emit(value: T) {
if (previousValue != value) {
previousValue = value
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
setValue(value)
}
} else {
setValue(value)
}
}
}
}
}
fun <T> Flow<T>.asFlowLiveData(
context: CoroutineContext = EmptyCoroutineContext,
defaultValue: T,
timeoutInMs: Long = DEFAULT_TIMEOUT,
): LiveData<T> = FlowLiveData(this, defaultValue, context, timeoutInMs)
fun <T> StateFlow<T>.asFlowLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
): LiveData<T> = FlowLiveData(this, value, context, timeoutInMs)

View File

@@ -1,50 +0,0 @@
package org.koitharu.kotatsu.core.util
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.EmptyCoroutineContext
class SingleLiveEvent<T> : LiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) {
if (pending.compareAndSet(true, false)) {
observer.onChanged(it)
}
}
}
override fun setValue(value: T) {
pending.set(true)
super.setValue(value)
}
@MainThread
fun call(newValue: T) {
setValue(newValue)
}
@AnyThread
fun postCall(newValue: T) {
postValue(newValue)
}
suspend fun emitCall(newValue: T) {
val dispatcher = Dispatchers.Main.immediate
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
setValue(newValue)
}
} else {
setValue(newValue)
}
}
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.util.ext
import androidx.annotation.AnyThread
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.core.util.Event
@Suppress("FunctionName")
fun <T> MutableEventFlow() = MutableStateFlow<Event<T>?>(null)
typealias EventFlow<T> = StateFlow<Event<T>?>
typealias MutableEventFlow<T> = MutableStateFlow<Event<T>?>
@AnyThread
fun <T> MutableEventFlow<T>.call(data: T) {
value = Event(data)
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transformLatest
import org.koitharu.kotatsu.R
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
@@ -52,3 +53,12 @@ fun <T> Flow<Collection<T>>.flatten(): Flow<T> = flow {
}
}
}
fun <T> Flow<T>.zipWithPrevious(): Flow<Pair<T?, T>> = flow {
var previous: T? = null
collect { value ->
val result = previous to value
previous = value
emit(result)
}
}

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.core.util.ext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.Event
fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) {
if (BuildConfig.DEBUG) {
require((this as? StateFlow)?.value !is Event<*>)
}
val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT
owner.lifecycleScope.launch(start = start) {
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect(collector)
}
}
}
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) {
owner.lifecycleScope.launch {
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect {
it?.consume(collector)
}
}
}
}

View File

@@ -1,32 +0,0 @@
package org.koitharu.kotatsu.core.util.ext
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.util.BufferedObserver
import kotlin.coroutines.EmptyCoroutineContext
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
"LiveData value is null"
}
fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: BufferedObserver<T>) {
var previous: T? = null
this.observe(owner) {
observer.onChanged(it, previous)
previous = it
}
}
suspend fun <T> MutableLiveData<T>.emitValue(newValue: T) {
val dispatcher = Dispatchers.Main.immediate
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
value = newValue
}
} else {
value = newValue
}
}

View File

@@ -6,23 +6,21 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.model.DoubleManga
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.IOException
import javax.inject.Inject
@Deprecated("")
class DetailsInteractor @Inject constructor(
private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository,
@@ -56,18 +54,6 @@ class DetailsInteractor @Inject constructor(
}
}
suspend fun deleteLocalManga(manga: Manga) {
val victim = if (manga.isLocal) manga else localMangaRepository.findSavedManga(manga)?.manga
checkNotNull(victim) { "Cannot find saved manga for ${manga.title}" }
val original = if (manga.isLocal) localMangaRepository.getRemoteManga(manga) else manga
localMangaRepository.delete(victim) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(victim, original)
}.onFailure {
it.printStackTraceDebug()
}
}
fun observeIncognitoMode(mangaFlow: Flow<Manga?>): Flow<Boolean> {
return mangaFlow
.distinctUntilChangedBy { it?.isNsfw }

View File

@@ -1,27 +1,26 @@
package org.koitharu.kotatsu.local.domain
package org.koitharu.kotatsu.details.domain
import dagger.Reusable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.core.model.DoubleManga
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
@Reusable
class DoubleMangaLoader @Inject constructor(
class DoubleMangaLoadUseCase @Inject constructor(
private val mangaDataRepository: MangaDataRepository,
private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
suspend fun load(manga: Manga): DoubleManga = coroutineScope {
suspend operator fun invoke(manga: Manga): DoubleManga = coroutineScope {
val remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) }
val localDeferred = async(Dispatchers.Default) { loadLocal(manga) }
DoubleManga(
@@ -30,14 +29,14 @@ class DoubleMangaLoader @Inject constructor(
)
}
suspend fun load(mangaId: Long): DoubleManga {
suspend operator fun invoke(mangaId: Long): DoubleManga {
val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE()
return load(manga)
return invoke(manga)
}
suspend fun load(intent: MangaIntent): DoubleManga {
suspend operator fun invoke(intent: MangaIntent): DoubleManga {
val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE()
return load(manga)
return invoke(manga)
}
private suspend fun loadLocal(manga: Manga): Result<Manga>? {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.model
package org.koitharu.kotatsu.details.domain.model
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter

View File

@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter

View File

@@ -18,10 +18,11 @@ import androidx.core.graphics.Insets
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
@@ -32,6 +33,8 @@ import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
@@ -90,10 +93,10 @@ class DetailsActivity :
ChaptersMenuProvider(viewModel, null)
}
viewModel.manga.observe(this, ::onMangaUpdated)
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
viewModel.onError.observeEvent(
this,
SnackbarErrorObserver(
host = viewBinding.containerDetails,
@@ -106,11 +109,11 @@ class DetailsActivity :
},
),
)
viewModel.onShowToast.observe(this) {
viewModel.onShowToast.observeEvent(this) {
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
}
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranchName.observe(this) {
viewModel.selectedBranch.observe(this) {
viewBinding.headerChapters?.subtitle = it
viewBinding.textViewSubtitle?.textAndVisible = it
}
@@ -124,7 +127,7 @@ class DetailsActivity :
viewBinding.buttonDropdown.isVisible = it.size > 1
}
viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted.observe(this, DownloadStartedObserver(viewBinding.containerDetails))
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.containerDetails))
addMenuProvider(
DetailsMenuProvider(
@@ -165,12 +168,12 @@ class DetailsActivity :
}
R.id.action_pages_thumbs -> {
val history = viewModel.historyInfo.value?.history
val history = viewModel.historyInfo.value.history
PagesThumbnailsSheet.show(
fm = supportFragmentManager,
manga = viewModel.manga.value ?: return false,
chapterId = history?.chapterId
?: viewModel.chapters.value?.firstOrNull()?.chapter?.id
?: viewModel.chapters.value.firstOrNull()?.chapter?.id
?: return false,
currentPage = history?.page ?: 0,
)
@@ -253,14 +256,14 @@ class DetailsActivity :
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setTitle(R.string.translations)
.setItems(viewModel.branches.value.orEmpty())
.setItems(viewModel.branches.value)
.create()
.also { it.show() }
}
private fun openReader(isIncognitoMode: Boolean) {
val manga = viewModel.manga.value ?: return
val chapterId = viewModel.historyInfo.value?.history?.chapterId
val chapterId = viewModel.historyInfo.value.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
snackbar.show()
@@ -301,11 +304,11 @@ class DetailsActivity :
private class PrefetchObserver(
private val context: Context,
) : Observer<List<ChapterListItem>?> {
) : FlowCollector<List<ChapterListItem>?> {
private var isCalled = false
override fun onChanged(value: List<ChapterListItem>?) {
override suspend fun emit(value: List<ChapterListItem>?) {
if (value.isNullOrEmpty()) {
return
}

View File

@@ -18,6 +18,7 @@ import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
@@ -34,6 +35,7 @@ import org.koitharu.kotatsu.core.util.ext.drawableTop
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.textAndVisible
@@ -42,7 +44,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga
@@ -82,7 +84,7 @@ class DetailsFragment :
binding.infoLayout.textViewSource.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)

View File

@@ -7,10 +7,7 @@ import android.text.style.ForegroundColorSpan
import androidx.core.net.toUri
import androidx.core.text.getSpans
import androidx.core.text.parseAsHtml
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -18,9 +15,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -31,27 +28,28 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.DoubleManga
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
@@ -69,7 +67,8 @@ class DetailsViewModel @Inject constructor(
private val downloadScheduler: DownloadWorker.Scheduler,
private val interactor: DetailsInteractor,
savedStateHandle: SavedStateHandle,
private val mangaLoader: DoubleMangaLoader,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
@@ -77,47 +76,46 @@ class DetailsViewModel @Inject constructor(
private val doubleManga: MutableStateFlow<DoubleManga?> = MutableStateFlow(intent.manga?.let { DoubleManga(it) })
private var loadingJob: Job
val onShowToast = SingleLiveEvent<Int>()
val onDownloadStarted = SingleLiveEvent<Unit>()
val onShowToast = MutableEventFlow<Int>()
val onDownloadStarted = MutableEventFlow<Unit>()
private val mangaData = doubleManga.map { it?.any }
val manga = doubleManga.map { it?.any }
.stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any)
private val history = historyRepository.observeOne(mangaId)
val history = historyRepository.observeOne(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private val favourite = interactor.observeIsFavourite(mangaId)
val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
private val newChapters = interactor.observeNewChapters(mangaId)
val newChaptersCount = interactor.observeNewChapters(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val chaptersQuery = MutableStateFlow("")
private val selectedBranch = MutableStateFlow<String?>(null)
val selectedBranch = MutableStateFlow<String?>(null)
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val manga = mangaData.filterNotNull().asLiveData(viewModelScope.coroutineContext)
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
val historyInfo: LiveData<HistoryInfo> = combine(
mangaData,
selectedBranch,
history,
interactor.observeIncognitoMode(mangaData),
) { m, b, h, im ->
HistoryInfo(m, b, h, im)
}.asFlowLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
defaultValue = HistoryInfo(null, null, null, false),
val isChaptersReversed = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_REVERSE_CHAPTERS,
valueProducer = { chaptersReverse },
)
val bookmarks = mangaData.flatMapLatest {
val historyInfo: StateFlow<HistoryInfo> = combine(
manga,
selectedBranch,
history,
interactor.observeIncognitoMode(manga),
) { m, b, h, im ->
HistoryInfo(m, b, h, im)
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.Eagerly,
initialValue = HistoryInfo(null, null, null, false),
)
val bookmarks = manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val localSize = doubleManga
.map {
@@ -128,9 +126,9 @@ class DetailsViewModel @Inject constructor(
} else {
0L
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
val description = mangaData
val description = manga
.distinctUntilChangedBy { it?.description.orEmpty() }
.transformLatest {
val description = it?.description
@@ -140,16 +138,16 @@ class DetailsViewModel @Inject constructor(
emit(description.parseAsHtml().filterSpans())
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
val onMangaRemoved = SingleLiveEvent<Manga>()
val onMangaRemoved = MutableEventFlow<Manga>()
val isScrobblingAvailable: Boolean
get() = scrobblers.any { it.isAvailable }
val scrobblingInfo: LiveData<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val branches: LiveData<List<MangaBranch>> = combine(
val branches: StateFlow<List<MangaBranch>> = combine(
doubleManga,
selectedBranch,
) { m, b ->
@@ -158,32 +156,29 @@ class DetailsViewModel @Inject constructor(
chapters.groupBy { x -> x.branch }
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
.sortedWith(BranchComparator())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val selectedBranchName = selectedBranch
.asFlowLiveData(viewModelScope.coroutineContext, null)
val isChaptersEmpty: LiveData<Boolean> = combine(
val isChaptersEmpty: StateFlow<Boolean> = combine(
doubleManga,
isLoading.asFlow(),
isLoading,
) { manga, loading ->
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
}.asFlowLiveData(viewModelScope.coroutineContext, false)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val chapters = combine(
combine(
doubleManga,
history,
selectedBranch,
newChapters,
newChaptersCount,
) { manga, history, branch, news ->
mapChapters(manga?.remote, manga?.local, history, news, branch)
},
chaptersReversed,
isChaptersReversed,
chaptersQuery,
) { list, reversed, query ->
(if (reversed) list.asReversed() else list).filterSearch(query)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val selectedBranchValue: String?
get() = selectedBranch.value
@@ -208,8 +203,8 @@ class DetailsViewModel @Inject constructor(
return
}
launchLoadingJob(Dispatchers.Default) {
interactor.deleteLocalManga(m)
onMangaRemoved.emitCall(m)
deleteLocalMangaUseCase(m)
onMangaRemoved.call(m)
}
}
@@ -276,12 +271,12 @@ class DetailsViewModel @Inject constructor(
doubleManga.requireValue().requireAny(),
chaptersIds,
)
onDownloadStarted.emitCall(Unit)
onDownloadStarted.call(Unit)
}
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
val result = mangaLoader.load(intent)
val result = doubleMangaLoadUseCase(intent)
val manga = result.requireAny()
// find default branch
val hist = historyRepository.getOne(manga)
@@ -317,7 +312,7 @@ class DetailsViewModel @Inject constructor(
}
private fun getScrobbler(index: Int): Scrobbler? {
val info = scrobblingInfo.value?.getOrNull(index)
val info = scrobblingInfo.value.getOrNull(index)
val scrobbler = if (info != null) {
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
} else {

View File

@@ -21,6 +21,8 @@ import org.koitharu.kotatsu.core.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
@@ -59,7 +61,7 @@ class ScrobblingInfoBottomSheet :
override fun onViewBindingCreated(binding: SheetScrobblingBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.onError.observe(viewLifecycleOwner) {
viewModel.onError.observeEvent(viewLifecycleOwner) {
Toast.makeText(binding.root.context, it.getDisplayMessage(binding.root.resources), Toast.LENGTH_SHORT)
.show()
}
@@ -105,7 +107,7 @@ class ScrobblingInfoBottomSheet :
when (v.id) {
R.id.button_menu -> menu?.show()
R.id.imageView_cover -> {
val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return
val coverUrl = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return
val options = scaleUpActivityOptionsOf(v)
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
}
@@ -135,7 +137,7 @@ class ScrobblingInfoBottomSheet :
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_browser -> {
val url = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.externalUrl ?: return false
val url = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.externalUrl ?: return false
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(
Intent.createChooser(intent, getString(R.string.open_in_browser)),
@@ -149,7 +151,7 @@ class ScrobblingInfoBottomSheet :
R.id.action_edit -> {
val manga = viewModel.manga.value ?: return false
val scrobblerService = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.scrobbler
val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService)
dismiss()
}

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.download.domain
import androidx.work.Data
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.Date

View File

@@ -11,14 +11,16 @@ import androidx.annotation.Px
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.lifecycle.Observer
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.worker.PausingReceiver
@@ -61,8 +63,8 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
viewModel.items.observe(this) {
downloadsAdapter.items = it
}
viewModel.onActionDone.observe(this, ReversibleActionObserver(viewBinding.recyclerView))
val menuObserver = Observer<Any> { _ -> invalidateOptionsMenu() }
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView))
val menuObserver = FlowCollector<Any> { _ -> invalidateOptionsMenu() }
viewModel.hasActiveWorks.observe(this, menuObserver)
viewModel.hasPausedWorks.observe(this, menuObserver)
viewModel.hasCancellableWorks.observe(this, menuObserver)

View File

@@ -20,8 +20,8 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
@@ -47,23 +47,23 @@ class DownloadsViewModel @Inject constructor(
.mapLatest { it.toDownloadsList() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val onActionDone = SingleLiveEvent<ReversibleAction>()
val onActionDone = MutableEventFlow<ReversibleAction>()
val items = works.map {
it?.toUiList() ?: listOf(LoadingState)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
val hasPausedWorks = works.map {
it?.any { x -> x.canResume } == true
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false)
val hasActiveWorks = works.map {
it?.any { x -> x.canPause } == true
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false)
val hasCancellableWorks = works.map {
it?.any { x -> !x.workState.isFinished } == true
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false)
fun cancel(id: UUID) {
launchJob(Dispatchers.Default) {
@@ -79,14 +79,14 @@ class DownloadsViewModel @Inject constructor(
workScheduler.cancel(work.id)
}
}
onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null))
onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null))
}
}
fun cancelAll() {
launchJob(Dispatchers.Default) {
workScheduler.cancelAll()
onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null))
onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null))
}
}
@@ -146,14 +146,14 @@ class DownloadsViewModel @Inject constructor(
workScheduler.delete(work.id)
}
}
onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null))
onActionDone.call(ReversibleAction(R.string.downloads_removed, null))
}
}
fun removeCompleted() {
launchJob(Dispatchers.Default) {
workScheduler.removeCompleted()
onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null))
onActionDone.call(ReversibleAction(R.string.downloads_removed, null))
}
}

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.download.ui.worker
import android.view.View
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
@@ -10,9 +10,9 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
class DownloadStartedObserver(
private val snackbarHost: View,
) : Observer<Unit> {
) : FlowCollector<Unit> {
override fun onChanged(value: Unit) {
override suspend fun emit(value: Unit) {
val snackbar = Snackbar.make(snackbarHost, R.string.download_started, Snackbar.LENGTH_LONG)
(snackbarHost.context.findActivity() as? BottomNavOwner)?.let {
snackbar.anchorView = it.bottomNav

View File

@@ -48,12 +48,12 @@ import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.core.util.progress.TimeLeftEstimator
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource

View File

@@ -4,7 +4,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable

View File

@@ -25,6 +25,8 @@ import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
@@ -74,9 +76,9 @@ class ExploreFragment :
viewModel.content.observe(viewLifecycleOwner) {
exploreAdapter?.items = it
}
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga)
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
viewModel.onShowSuggestionsTip.observe(viewLifecycleOwner) {
showSuggestionsTip()

View File

@@ -1,16 +1,17 @@
package org.koitharu.kotatsu.explore.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -18,8 +19,8 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.parsers.model.Manga
@@ -34,29 +35,28 @@ class ExploreViewModel @Inject constructor(
private val exploreRepository: ExploreRepository,
) : BaseViewModel() {
private val gridMode = settings.observeAsStateFlow(
val isGrid = settings.observeAsStateFlow(
key = AppSettings.KEY_SOURCES_GRID,
scope = viewModelScope + Dispatchers.IO,
valueProducer = { isSourcesGridMode },
)
val onOpenManga = SingleLiveEvent<Manga>()
val onActionDone = SingleLiveEvent<ReversibleAction>()
val onShowSuggestionsTip = SingleLiveEvent<Unit>()
val isGrid = gridMode.asFlowLiveData(viewModelScope.coroutineContext)
val onOpenManga = MutableEventFlow<Manga>()
val onActionDone = MutableEventFlow<ReversibleAction>()
val onShowSuggestionsTip = MutableEventFlow<Unit>()
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
val content: StateFlow<List<ExploreItem>> = isLoading.flatMapLatest { loading ->
if (loading) {
flowOf(listOf(ExploreItem.Loading))
} else {
createContentFlow()
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(ExploreItem.Loading))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(ExploreItem.Loading))
init {
launchJob(Dispatchers.Default) {
if (!settings.isSuggestionsEnabled && settings.isTipEnabled(TIP_SUGGESTIONS)) {
onShowSuggestionsTip.emitCall(Unit)
onShowSuggestionsTip.call(Unit)
}
}
}
@@ -64,7 +64,7 @@ class ExploreViewModel @Inject constructor(
fun openRandom() {
launchLoadingJob(Dispatchers.Default) {
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
onOpenManga.emitCall(manga)
onOpenManga.call(manga)
}
}
@@ -74,7 +74,7 @@ class ExploreViewModel @Inject constructor(
val rollback = ReversibleHandle {
settings.hiddenSources -= source.name
}
onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback))
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
}
}
@@ -95,7 +95,7 @@ class ExploreViewModel @Inject constructor(
}
.onStart { emit("") }
.map { settings.getMangaSources(includeHidden = false) }
.combine(gridMode) { content, grid -> buildList(content, grid) }
.combine(isGrid) { content, grid -> buildList(content, grid) }
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ExploreItem> {
val result = ArrayList<ExploreItem>(sources.size + 3)

View File

@@ -24,6 +24,8 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
@@ -71,7 +73,7 @@ class FavouriteCategoriesActivity :
onBackPressedDispatcher.addCallback(exitReorderModeCallback)
viewModel.detalizedCategories.observe(this, ::onCategoriesChanged)
viewModel.onError.observe(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.isInReorderMode.observe(this, ::onReorderModeChanged)
}

View File

@@ -1,17 +1,17 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
@@ -27,23 +27,11 @@ class FavouritesCategoriesViewModel @Inject constructor(
) : BaseViewModel() {
private var reorderJob: Job? = null
private val isReorder = MutableStateFlow(false)
val isInReorderMode = isReorder.asLiveData(viewModelScope.coroutineContext)
val allCategories = repository.observeCategories()
.mapItems {
CategoryListModel(
mangaCount = 0,
covers = listOf(),
category = it,
isReorderMode = false,
)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val isInReorderMode = MutableStateFlow(false)
val detalizedCategories = combine(
repository.observeCategoriesWithCovers(),
isReorder,
isInReorderMode,
) { list, reordering ->
list.map { (category, covers) ->
CategoryListModel(
@@ -62,7 +50,7 @@ class FavouritesCategoriesViewModel @Inject constructor(
),
)
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun deleteCategory(id: Long) {
launchJob {
@@ -80,12 +68,12 @@ class FavouritesCategoriesViewModel @Inject constructor(
settings.isAllFavouritesVisible = isVisible
}
fun isInReorderMode(): Boolean = isReorder.value
fun isInReorderMode(): Boolean = isInReorderMode.value
fun isEmpty(): Boolean = detalizedCategories.value?.none { it is CategoryListModel } ?: true
fun isEmpty(): Boolean = detalizedCategories.value.none { it is CategoryListModel }
fun setReorderMode(isReorderMode: Boolean) {
isReorder.value = isReorderMode
isInReorderMode.value = isReorderMode
}
fun reorderCategories(oldPos: Int, newPos: Int) {

View File

@@ -22,6 +22,8 @@ import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getSerializableCompat
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
@@ -50,10 +52,10 @@ class FavouritesCategoryEditActivity :
viewBinding.editName.addTextChangedListener(this)
afterTextChanged(viewBinding.editName.text)
viewModel.onSaved.observe(this) { finishAfterTransition() }
viewModel.onSaved.observeEvent(this) { finishAfterTransition() }
viewModel.category.observe(this, ::onCategoryChanged)
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
viewModel.onError.observe(this, ::onError)
viewModel.onError.observeEvent(this, ::onError)
viewModel.isTrackerEnabled.observe(this) {
viewBinding.switchTracker.isVisible = it
}

View File

@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.favourites.ui.categories.edit
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.EXTRA_ID
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.NO_ID
@@ -26,22 +29,20 @@ class FavouritesCategoryEditViewModel @Inject constructor(
private val categoryId = savedStateHandle[EXTRA_ID] ?: NO_ID
val onSaved = SingleLiveEvent<Unit>()
val category = MutableLiveData<FavouriteCategory?>()
val onSaved = MutableEventFlow<Unit>()
val category = MutableStateFlow<FavouriteCategory?>(null)
val isTrackerEnabled = liveData(viewModelScope.coroutineContext + Dispatchers.Default) {
val isTrackerEnabled = flow {
emit(settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
init {
launchLoadingJob(Dispatchers.Default) {
category.emitValue(
if (categoryId != NO_ID) {
repository.getCategory(categoryId)
} else {
null
},
)
category.value = if (categoryId != NO_ID) {
repository.getCategory(categoryId)
} else {
null
}
}
}
@@ -58,7 +59,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
} else {
repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled, isVisibleOnShelf)
}
onSaved.emitCall(Unit)
onSaved.call(Unit)
}
}
}

View File

@@ -15,6 +15,8 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
@@ -46,7 +48,7 @@ class FavouriteCategoriesBottomSheet :
binding.headerBar.toolbar.setOnMenuItemClickListener(this)
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
}
override fun onDestroyView() {

View File

@@ -4,11 +4,13 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
@@ -33,7 +35,7 @@ class MangaCategoriesViewModel @Inject constructor(
isChecked = it.id in checked,
)
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity

View File

@@ -1,25 +1,28 @@
package org.koitharu.kotatsu.favourites.ui.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -43,12 +46,12 @@ class FavouritesListViewModel @Inject constructor(
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
val sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
MutableLiveData(null)
val sortOrder: StateFlow<SortOrder?> = if (categoryId == NO_ID) {
MutableStateFlow(null)
} else {
repository.observeCategory(categoryId)
.map { it?.order }
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
}
override val content = combine(
@@ -57,7 +60,7 @@ class FavouritesListViewModel @Inject constructor(
} else {
repository.observeAll(categoryId)
},
listModeFlow,
listMode,
) { list, mode ->
when {
list.isEmpty() -> listOf(
@@ -77,7 +80,7 @@ class FavouritesListViewModel @Inject constructor(
}
}.catch {
emit(listOf(it.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
override fun onRefresh() = Unit
@@ -93,7 +96,7 @@ class FavouritesListViewModel @Inject constructor(
} else {
repository.removeFromCategory(categoryId, ids)
}
onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle))
onActionDone.call(ReversibleAction(R.string.removed_from_favourites, handle))
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.history.domain
package org.koitharu.kotatsu.history.data
import androidx.room.withTransaction
import dagger.Reusable
@@ -17,8 +17,7 @@ import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.history.data.toMangaHistory
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.history.domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject
class HistoryUpdateUseCase @Inject constructor(
private val historyRepository: HistoryRepository,
) {
suspend operator fun invoke(manga: Manga, readerState: ReaderState, percent: Float) {
historyRepository.addOrUpdate(
manga = manga,
chapterId = readerState.chapterId,
page = readerState.page,
scroll = readerState.scroll,
percent = percent,
)
}
fun invokeAsync(
manga: Manga,
readerState: ReaderState,
percent: Float
) = processLifecycleScope.launch(Dispatchers.Default) {
runCatchingCancellable {
invoke(manga, readerState, percent)
}.onFailure {
it.printStackTraceDebug()
}
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.history.domain
package org.koitharu.kotatsu.history.domain.model
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.parsers.model.Manga
@@ -6,4 +6,4 @@ import org.koitharu.kotatsu.parsers.model.Manga
data class MangaWithHistory(
val manga: Manga,
val history: MangaHistory
)
)

View File

@@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource

View File

@@ -1,28 +1,28 @@
package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -45,15 +45,16 @@ class HistoryListViewModel @Inject constructor(
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
val isGroupingEnabled = MutableLiveData<Boolean>()
private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled }
.onEach { isGroupingEnabled.emitValue(it) }
val isGroupingEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_HISTORY_GROUPING,
valueProducer = { isHistoryGroupingEnabled },
)
override val content = combine(
repository.observeAllWithHistory(),
historyGrouping,
listModeFlow,
isGroupingEnabled,
listMode,
) { list, grouped, mode ->
when {
list.isEmpty() -> listOf(
@@ -73,7 +74,7 @@ class HistoryListViewModel @Inject constructor(
loadingCounter.decrement()
}.catch {
emit(listOf(it.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
override fun onRefresh() = Unit
@@ -91,7 +92,7 @@ class HistoryListViewModel @Inject constructor(
}
launchJob(Dispatchers.Default) {
val handle = repository.delete(ids)
onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle))
onActionDone.call(ReversibleAction(R.string.removed_from_history, handle))
}
}

View File

@@ -13,7 +13,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.ColorUtils
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.scale
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
class ReadingProgressDrawable(
context: Context,

View File

@@ -12,7 +12,7 @@ import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
class ReadingProgressView @JvmOverloads constructor(
context: Context,

View File

@@ -37,6 +37,8 @@ import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
@@ -123,9 +125,9 @@ abstract class MangaListFragment :
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.onDownloadStarted.observe(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView))
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView))
}
override fun onDestroyView() {

View File

@@ -1,39 +1,38 @@
package org.koitharu.kotatsu.list.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
abstract class MangaListViewModel(
private val settings: AppSettings,
settings: AppSettings,
private val downloadScheduler: DownloadWorker.Scheduler,
) : BaseViewModel() {
abstract val content: LiveData<List<ListModel>>
protected val listModeFlow = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, settings.listMode)
val listMode = listModeFlow.asFlowLiveData(viewModelScope.coroutineContext)
val onActionDone = SingleLiveEvent<ReversibleAction>()
val gridScale = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
abstract val content: StateFlow<List<ListModel>>
val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.listMode)
val onActionDone = MutableEventFlow<ReversibleAction>()
val gridScale = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_GRID_SIZE,
valueProducer = { gridSize / 100f },
)
val onDownloadStarted = SingleLiveEvent<Unit>()
val onDownloadStarted = MutableEventFlow<Unit>()
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
@@ -44,7 +43,7 @@ abstract class MangaListViewModel(
fun download(items: Set<Manga>) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(items)
onDownloadStarted.emitCall(Unit)
onDownloadStarted.call(Unit)
}
}
}

View File

@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel

View File

@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
import org.koitharu.kotatsu.parsers.model.MangaTag

View File

@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentFragmentViewModels
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel

View File

@@ -1,22 +1,25 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.text.Collator
@@ -31,13 +34,13 @@ class FilterCoordinator(
private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet()))
private var searchQuery = MutableStateFlow("")
private val localTagsDeferred = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
private val localTags = SuspendLazy {
dataRepository.findTags(repository.source)
}
private var availableTagsDeferred = loadTagsAsync()
val items: LiveData<List<FilterItem>> = getItemsFlow()
.asFlowLiveData(coroutineScope.coroutineContext + Dispatchers.Default, listOf(FilterItem.Loading))
val items: StateFlow<List<FilterItem>> = getItemsFlow()
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(FilterItem.Loading))
init {
observeState()
@@ -97,7 +100,7 @@ class FilterCoordinator(
}
private fun getTagsAsFlow() = flow {
val localTags = localTagsDeferred.await()
val localTags = localTags.get()
emit(TagsWrapper(localTags, isLoading = true, isError = false))
val remoteTags = tryLoadTags()
if (remoteTags == null) {

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.Manga

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.local.domain
package org.koitharu.kotatsu.local.data
import android.net.Uri
import androidx.core.net.toFile
@@ -15,13 +15,10 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.CompositeMutex
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.data.output.LocalMangaUtil
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage

View File

@@ -16,10 +16,10 @@ import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.util.ext.resolveName
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.domain.model.LocalManga
import java.io.File
import java.io.IOException
import javax.inject.Inject

View File

@@ -9,9 +9,9 @@ import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.toListSorted
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.ImageFileFilter
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.local.data.input
import android.net.Uri
import androidx.core.net.toFile
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage

View File

@@ -10,9 +10,9 @@ import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.readText
import org.koitharu.kotatsu.core.util.ext.toListSorted
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.local.domain
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.IOException
import javax.inject.Inject
class DeleteLocalMangaUseCase @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
) {
suspend operator fun invoke(manga: Manga) {
val victim = if (manga.isLocal) manga else localMangaRepository.findSavedManga(manga)?.manga
checkNotNull(victim) { "Cannot find saved manga for ${manga.title}" }
val original = if (manga.isLocal) localMangaRepository.getRemoteManga(manga) else manga
localMangaRepository.delete(victim) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(victim, original)
}.onFailure {
it.printStackTraceDebug()
}
}
suspend operator fun invoke(ids: Set<Long>) {
val list = localMangaRepository.getList(0, null, null)
var removed = 0
for (manga in list) {
if (manga.id in ids) {
invoke(manga)
removed++
}
}
check(removed == ids.size) {
"Removed $removed files but ${ids.size} requested"
}
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.local.data
package org.koitharu.kotatsu.local.domain.model
import androidx.core.net.toFile
import androidx.core.net.toUri
@@ -38,9 +38,7 @@ class LocalManga(
other as LocalManga
if (manga != other.manga) return false
if (file != other.file) return false
return true
return file == other.file
}
override fun hashCode(): Int {

View File

@@ -15,9 +15,9 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -26,7 +27,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))
viewModel.onMangaRemoved.observe(viewLifecycleOwner) { onItemRemoved() }
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
}
override fun onEmptyActionClick() {

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.local.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
@@ -10,18 +8,20 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -29,15 +29,14 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import java.io.IOException
import java.util.LinkedList
import javax.inject.Inject
@@ -49,11 +48,12 @@ class LocalListViewModel @Inject constructor(
private val settings: AppSettings,
private val tagHighlighter: MangaTagHighlighter,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
val onMangaRemoved = SingleLiveEvent<Unit>()
val sortOrder = MutableLiveData(settings.localListOrder)
val onMangaRemoved = MutableEventFlow<Unit>()
val sortOrder = MutableStateFlow(settings.localListOrder)
private val listError = MutableStateFlow<Throwable?>(null)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
@@ -61,8 +61,8 @@ class LocalListViewModel @Inject constructor(
override val content = combine(
mangaList,
listModeFlow,
sortOrder.asFlow(),
listMode,
sortOrder,
selectedTags,
listError,
) { list, mode, order, tags, error ->
@@ -83,7 +83,7 @@ class LocalListViewModel @Inject constructor(
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
}
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
onRefresh()
@@ -120,18 +120,8 @@ class LocalListViewModel @Inject constructor(
fun delete(ids: Set<Long>) {
launchLoadingJob(Dispatchers.Default) {
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.update { list ->
list?.filterNot { it.id == manga.id }
}
}
onMangaRemoved.emitCall(Unit)
deleteLocalMangaUseCase(ids)
onMangaRemoved.call(Unit)
}
}

View File

@@ -11,7 +11,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import java.util.concurrent.TimeUnit
@HiltWorker

View File

@@ -45,6 +45,8 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.hideKeyboard
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.resolve
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
@@ -137,8 +139,8 @@ class MainActivity :
onFirstStart()
}
viewModel.onOpenReader.observe(this, this::onOpenReader)
viewModel.onError.observe(this, SnackbarErrorObserver(viewBinding.container, null))
viewModel.onOpenReader.observeEvent(this, this::onOpenReader)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null))
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged)

View File

@@ -5,17 +5,20 @@ import androidx.core.util.set
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject
@@ -24,21 +27,25 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
private val appUpdateRepository: AppUpdateRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
trackingRepository: TrackingRepository,
settings: AppSettings,
) : BaseViewModel() {
val onOpenReader = SingleLiveEvent<Manga>()
val onOpenReader = MutableEventFlow<Manga>()
val isResumeEnabled = combine(
historyRepository.observeHasItems(),
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
) { hasItems, incognito ->
hasItems && !incognito
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false,
)
val isFeedAvailable = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
val isFeedAvailable = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_TRACKER_ENABLED,
valueProducer = { isTrackerEnabled },
)
@@ -51,7 +58,11 @@ class MainViewModel @Inject constructor(
a[R.id.nav_tools] = if (appUpdate != null) 1 else 0
a[R.id.nav_feed] = tracks
a
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0))
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SparseIntArray(0),
)
init {
launchJob {
@@ -62,7 +73,7 @@ class MainViewModel @Inject constructor(
fun openLastReader() {
launchLoadingJob(Dispatchers.Default) {
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
onOpenReader.emitCall(manga)
onOpenReader.call(manga)
}
}
}

View File

@@ -22,6 +22,8 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityProtectBinding
@AndroidEntryPoint
@@ -42,9 +44,9 @@ class ProtectActivity :
viewBinding.buttonNext.setOnClickListener(this)
viewBinding.buttonCancel.setOnClickListener(this)
viewModel.onError.observe(this, this::onError)
viewModel.onError.observeEvent(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onUnlockSuccess.observe(this) {
viewModel.onUnlockSuccess.observeEvent(this) {
val intent = intent.getParcelableExtraCompat<Intent>(EXTRA_INTENT)
startActivity(intent)
finishAfterTransition()

View File

@@ -6,7 +6,8 @@ import kotlinx.coroutines.delay
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.parsers.util.md5
import javax.inject.Inject
@@ -20,7 +21,7 @@ class ProtectViewModel @Inject constructor(
private var job: Job? = null
val onUnlockSuccess = SingleLiveEvent<Unit>()
val onUnlockSuccess = MutableEventFlow<Unit>()
val isBiometricEnabled
get() = settings.isBiometricProtectionEnabled

View File

@@ -4,8 +4,8 @@ import android.util.LongSparseArray
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.model.DoubleManga
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject

View File

@@ -0,0 +1,97 @@
package org.koitharu.kotatsu.reader.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.model.findChapter
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject
import kotlin.math.roundToInt
class DetectReaderModeUseCase @Inject constructor(
private val dataRepository: MangaDataRepository,
private val settings: AppSettings,
private val mangaRepositoryFactory: MangaRepository.Factory,
@MangaHttpClient private val okHttpClient: OkHttpClient,
) {
suspend operator fun invoke(manga: Manga, state: ReaderState?): ReaderMode {
dataRepository.getReaderMode(manga.id)?.let { return it }
val defaultMode = settings.defaultReaderMode
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
return defaultMode
}
val chapter = state?.let { manga.findChapter(it.chapterId) }
?: manga.chapters?.firstOrNull()
?: error("There are no chapters in this manga")
val repo = mangaRepositoryFactory.create(manga.source)
val pages = repo.getPages(chapter)
return runCatchingCancellable {
val isWebtoon = guessMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess {
dataRepository.saveReaderMode(manga, it)
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(defaultMode)
}
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
private suspend fun guessMangaIsWebtoon(repository: MangaRepository, pages: List<MangaPage>): Boolean {
val pageIndex = (pages.size * 0.3).roundToInt()
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = PageLoader.createPageRequest(page, url)
okHttpClient.newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * MIN_WEBTOON_RATIO < size.height
}
companion object {
private const val MIN_WEBTOON_RATIO = 1.8
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher
@@ -15,7 +16,6 @@ import okio.IOException
import okio.buffer
import okio.sink
import okio.source
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
@@ -74,7 +74,7 @@ class PageSaveHelper @Inject constructor(
var extension = name.substringAfterLast('.', "")
name = name.substringBeforeLast('.')
if (extension.length !in 2..4) {
val mimeType = MangaDataRepository.getImageMimeType(file)
val mimeType = getImageMimeType(file)
extension = if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
} else {
@@ -83,4 +83,12 @@ class PageSaveHelper @Inject constructor(
}
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
}
private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
}

View File

@@ -42,9 +42,11 @@ import org.koitharu.kotatsu.core.util.IdlingDetector
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
import org.koitharu.kotatsu.core.util.ext.isRtl
import org.koitharu.kotatsu.core.util.ext.observeWithPrevious
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -108,7 +110,7 @@ class ReaderActivity :
insetsDelegate.interceptingWindowInsetsListener = this
idlingDetector.bindToLifecycle(this)
viewModel.onError.observe(
viewModel.onError.observeEvent(
this,
DialogErrorObserver(
host = viewBinding.container,
@@ -117,23 +119,23 @@ class ReaderActivity :
onResolved = { isResolved ->
if (isResolved) {
viewModel.reload()
} else if (viewModel.content.value?.pages.isNullOrEmpty()) {
} else if (viewModel.content.value.pages.isEmpty()) {
finishAfterTransition()
}
},
),
)
viewModel.readerMode.observe(this, this::onInitReader)
viewModel.onPageSaved.observe(this, this::onPageSaved)
viewModel.uiState.observeWithPrevious(this, this::onUiStateChanged)
viewModel.onPageSaved.observeEvent(this, this::onPageSaved)
viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value == true)
onLoadingStateChanged(viewModel.isLoading.value)
}
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
viewModel.onShowToast.observe(this) { msgId ->
viewModel.onShowToast.observeEvent(this) { msgId ->
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
.setAnchorView(viewBinding.appbarBottom)
.show()
@@ -150,7 +152,10 @@ class ReaderActivity :
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
}
private fun onInitReader(mode: ReaderMode) {
private fun onInitReader(mode: ReaderMode?) {
if (mode == null) {
return
}
if (readerManager.currentMode != mode) {
readerManager.replace(mode)
}
@@ -190,7 +195,7 @@ class ReaderActivity :
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value == true) {
if (viewModel.isBookmarkAdded.value) {
viewModel.removeBookmark()
} else {
viewModel.addBookmark()
@@ -209,7 +214,7 @@ class ReaderActivity :
}
private fun onLoadingStateChanged(isLoading: Boolean) {
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
val hasPages = viewModel.content.value.pages.isNotEmpty()
viewBinding.layoutLoading.isVisible = isLoading && !hasPages
if (isLoading && hasPages) {
viewBinding.toastView.show(R.string.loading_)
@@ -260,7 +265,7 @@ class ReaderActivity :
override fun onPageSelected(page: ReaderPage) {
lifecycleScope.launch(Dispatchers.Default) {
val pages = viewModel.content.value?.pages ?: return@launch
val pages = viewModel.content.value.pages
val index = pages.indexOfFirst { it.chapterId == page.chapterId && it.id == page.id }
if (index != -1) {
withContext(Dispatchers.Main) {
@@ -311,7 +316,7 @@ class ReaderActivity :
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
viewBinding.appbarTop.isVisible = isUiVisible
viewBinding.appbarBottom?.isVisible = isUiVisible
viewBinding.infoBar.isGone = isUiVisible || (viewModel.isInfoBarEnabled.value == false)
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
if (isUiVisible) {
showSystemUI()
} else {
@@ -367,7 +372,8 @@ class ReaderActivity :
menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
}
private fun onUiStateChanged(uiState: ReaderUiState?, previous: ReaderUiState?) {
private fun onUiStateChanged(pair: Pair<ReaderUiState?, ReaderUiState?>) {
val (uiState: ReaderUiState?, previous: ReaderUiState?) = pair
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
viewBinding.infoBar.update(uiState)
if (uiState == null) {

View File

@@ -5,8 +5,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -28,35 +26,32 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.DoubleManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
@@ -70,7 +65,6 @@ private const val PREFETCH_LIMIT = 10
@HiltViewModel
class ReaderViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository,
private val bookmarksRepository: BookmarksRepository,
@@ -79,7 +73,9 @@ class ReaderViewModel @Inject constructor(
private val pageLoader: PageLoader,
private val chaptersLoader: ChaptersLoader,
private val shortcutsUpdater: ShortcutsUpdater,
private val mangaLoader: DoubleMangaLoader,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
private val historyUpdateUseCase: HistoryUpdateUseCase,
private val detectReaderModeUseCase: DetectReaderModeUseCase,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
@@ -95,29 +91,29 @@ class ReaderViewModel @Inject constructor(
private val mangaFlow: Flow<Manga?>
get() = mangaData.map { it?.any }
val readerMode = MutableLiveData<ReaderMode>()
val onPageSaved = SingleLiveEvent<Uri?>()
val onShowToast = SingleLiveEvent<Int>()
val uiState = MutableLiveData<ReaderUiState?>(null)
val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Uri?>()
val onShowToast = MutableEventFlow<Int>()
val uiState = MutableStateFlow<ReaderUiState?>(null)
val content = MutableLiveData(ReaderContent(emptyList(), null))
val content = MutableStateFlow(ReaderContent(emptyList(), null))
val manga: DoubleManga?
get() = mangaData.value
val readerAnimation = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
val readerAnimation = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_ANIMATION,
valueProducer = { readerAnimation },
)
val isInfoBarEnabled = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
val isInfoBarEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_BAR,
valueProducer = { isReaderBarEnabled },
)
val isWebtoonZoomEnabled = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
val isWebtoonZoomEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_WEBTOON_ZOOM,
valueProducer = { isWebtoonZoomEnable },
)
@@ -136,9 +132,9 @@ class ReaderViewModel @Inject constructor(
) { manga, policy ->
policy == ScreenshotsPolicy.BLOCK_ALL ||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isBookmarkAdded: LiveData<Boolean> = currentState.flatMapLatest { state ->
val isBookmarkAdded = currentState.flatMapLatest { state ->
val manga = mangaData.value?.any
if (state == null || manga == null) {
flowOf(false)
@@ -146,7 +142,7 @@ class ReaderViewModel @Inject constructor(
bookmarksRepository.observeBookmark(manga, state.chapterId, state.page)
.map { it != null }
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
init {
loadImpl()
@@ -173,10 +169,8 @@ class ReaderViewModel @Inject constructor(
mode = newMode,
)
readerMode.value = newMode
content.value?.run {
content.value = copy(
state = getCurrentState(),
)
content.update {
it.copy(state = getCurrentState())
}
}
}
@@ -189,9 +183,9 @@ class ReaderViewModel @Inject constructor(
return
}
val readerState = state ?: currentState.value ?: return
historyRepository.saveStateAsync(
historyUpdateUseCase.invokeAsync(
manga = mangaData.value?.any ?: return,
state = readerState,
readerState = readerState,
percent = computePercent(readerState.chapterId, readerState.page),
)
}
@@ -212,12 +206,12 @@ class ReaderViewModel @Inject constructor(
prevJob?.cancelAndJoin()
try {
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
onPageSaved.emitCall(dest)
onPageSaved.call(dest)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
e.printStackTraceDebug()
onPageSaved.emitCall(null)
onPageSaved.call(null)
}
}
}
@@ -233,7 +227,7 @@ class ReaderViewModel @Inject constructor(
fun getCurrentPage(): MangaPage? {
val state = currentState.value ?: return null
return content.value?.pages?.find {
return content.value.pages.find {
it.chapterId == state.chapterId && it.index == state.page
}?.toMangaPage()
}
@@ -242,9 +236,9 @@ class ReaderViewModel @Inject constructor(
val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
content.postValue(ReaderContent(emptyList(), null))
content.value = ReaderContent(emptyList(), null)
chaptersLoader.loadSingleChapter(id)
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0)))
content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0))
}
}
@@ -254,7 +248,7 @@ class ReaderViewModel @Inject constructor(
stateChangeJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
loadingJob?.join()
val pages = content.value?.pages ?: return@launchJob
val pages = content.value.pages
pages.getOrNull(position)?.let { page ->
currentState.update { cs ->
cs?.copy(chapterId = page.chapterId, page = page.index)
@@ -296,7 +290,7 @@ class ReaderViewModel @Inject constructor(
percent = computePercent(state.chapterId, state.page),
)
bookmarksRepository.addBookmark(bookmark)
onShowToast.emitCall(R.string.bookmark_added)
onShowToast.call(R.string.bookmark_added)
}
}
@@ -318,32 +312,31 @@ class ReaderViewModel @Inject constructor(
var manga =
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", ""))
mangaData.value = manga
manga = mangaLoader.load(intent)
manga = doubleMangaLoadUseCase(intent)
chaptersLoader.init(manga)
// determine mode
val singleManga = manga.requireAny()
val mode = detectReaderMode(singleManga)
// obtain state
if (currentState.value == null) {
currentState.value = historyRepository.getOne(singleManga)?.let {
ReaderState(it)
} ?: ReaderState(singleManga, preselectedBranch)
}
val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value)
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
mangaData.value = manga.filterChapters(branch)
readerMode.emitValue(mode)
readerMode.value = mode
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
// save state
if (!isIncognito) {
currentState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyRepository.addOrUpdate(singleManga, it.chapterId, it.page, it.scroll, percent)
historyUpdateUseCase.invoke(singleManga, it, percent)
}
}
notifyStateChanged()
content.emitValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
content.value = ReaderContent(chaptersLoader.snapshot(), currentState.value)
}
}
@@ -353,7 +346,7 @@ class ReaderViewModel @Inject constructor(
loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.join()
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
content.value = ReaderContent(chaptersLoader.snapshot(), null)
}
}
@@ -367,27 +360,6 @@ class ReaderViewModel @Inject constructor(
}
}
private suspend fun detectReaderMode(manga: Manga): ReaderMode {
dataRepository.getReaderMode(manga.id)?.let { return it }
val defaultMode = settings.defaultReaderMode
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
return defaultMode
}
val chapter = currentState.value?.chapterId?.let { chaptersLoader.peekChapter(it) }
?: manga.chapters?.randomOrNull()
?: error("There are no chapters in this manga")
val repo = mangaRepositoryFactory.create(manga.source)
val pages = repo.getPages(chapter)
return runCatchingCancellable {
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess {
dataRepository.saveReaderMode(manga, it)
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(defaultMode)
}
@WorkerThread
private fun notifyStateChanged() {
val state = getCurrentState()
@@ -402,7 +374,7 @@ class ReaderViewModel @Inject constructor(
isSliderEnabled = settings.isReaderSliderEnabled,
percent = if (state != null) computePercent(state.chapterId, state.page) else PROGRESS_NONE,
)
uiState.postValue(newState)
uiState.value = newState
}
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
@@ -419,23 +391,3 @@ class ReaderViewModel @Inject constructor(
return ppc * chapterIndex + ppc * pagePercent
}
}
/**
* This function is not a member of the ReaderViewModel
* because it should work independently of the ViewModel's lifecycle.
*/
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
return processLifecycleScope.launch(Dispatchers.Default) {
runCatchingCancellable {
addOrUpdate(
manga = manga,
chapterId = state.chapterId,
page = state.page,
scroll = state.scroll,
percent = percent,
)
}.onFailure {
it.printStackTraceDebug()
}
}
}

View File

@@ -24,6 +24,8 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.indicator
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.Manga
@@ -64,7 +66,7 @@ class ColorFilterConfigActivity :
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
viewModel.isLoading.observe(this, this::onLoadingChanged)
viewModel.preview.observe(this, this::onPreviewChanged)
viewModel.onDismiss.observe(this) {
viewModel.onDismiss.observeEvent(this) {
finishAfterTransition()
}
}

View File

@@ -5,6 +5,7 @@ import android.content.DialogInterface
import androidx.activity.OnBackPressedCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.call
class ColorFilterConfigBackPressedDispatcher(
private val context: Context,

View File

@@ -1,16 +1,16 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
@@ -26,9 +26,9 @@ class ColorFilterConfigViewModel @Inject constructor(
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
private var initialColorFilter: ReaderColorFilter? = null
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
val onDismiss = SingleLiveEvent<Unit>()
val preview = MutableLiveData<MangaPage?>(null)
val colorFilter = MutableStateFlow<ReaderColorFilter?>(null)
val onDismiss = MutableEventFlow<Unit>()
val preview = MutableStateFlow<MangaPage?>(null)
val isChanged: Boolean
get() = colorFilter.value != initialColorFilter
@@ -44,13 +44,11 @@ class ColorFilterConfigViewModel @Inject constructor(
launchLoadingJob(Dispatchers.Default) {
val repository = mangaRepositoryFactory.create(page.source)
val url = repository.getPageUrl(page)
preview.emitValue(
MangaPage(
id = page.id,
url = url,
preview = page.preview,
source = page.source,
),
preview.value = MangaPage(
id = page.id,
url = url,
preview = page.preview,
source = page.source,
)
}
}
@@ -72,7 +70,7 @@ class ColorFilterConfigViewModel @Inject constructor(
fun save() {
launchLoadingJob(Dispatchers.Default) {
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
onDismiss.emitCall(Unit)
onDismiss.call(Unit)
}
}
}

View File

@@ -18,12 +18,14 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
@@ -75,8 +77,8 @@ class ReaderConfigBottomSheet :
binding.sliderTimer.addOnChangeListener(this)
binding.switchScrollTimer.setOnCheckedChangeListener(this)
settings.observeAsLiveData(
context = lifecycleScope.coroutineContext + Dispatchers.Default,
settings.observeAsStateFlow(
scope = lifecycleScope + Dispatchers.Default,
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
valueProducer = { readerAutoscrollSpeed },
).observe(viewLifecycleOwner) {

View File

@@ -6,6 +6,7 @@ import androidx.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope

View File

@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader

View File

@@ -20,6 +20,8 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.ScrollListenerInvalidationObserver
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetPagesBinding
@@ -93,7 +95,7 @@ class PagesThumbnailsSheet :
viewModel.branch.observe(viewLifecycleOwner) {
onExpansionStateChanged(binding.headerBar, binding.headerBar.isExpanded)
}
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
}
override fun onDestroyView() {

View File

@@ -1,18 +1,17 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
import javax.inject.Inject
@@ -22,7 +21,7 @@ class PagesThumbnailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
private val chaptersLoader: ChaptersLoader,
private val mangaLoader: DoubleMangaLoader,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
) : BaseViewModel() {
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
@@ -31,9 +30,9 @@ class PagesThumbnailsViewModel @Inject constructor(
private val repository = mangaRepositoryFactory.create(manga.source)
private val mangaDetails = SuspendLazy {
mangaLoader.load(manga).let {
doubleMangaLoadUseCase(manga).let {
val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch
branch.emitValue(b)
branch.value = b
it.filterChapters(b)
}
}
@@ -41,8 +40,8 @@ class PagesThumbnailsViewModel @Inject constructor(
private var loadingPrevJob: Job? = null
private var loadingNextJob: Job? = null
val thumbnails = MutableLiveData<List<ListModel>>()
val branch = MutableLiveData<String?>()
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
val branch = MutableStateFlow<String?>(null)
val title = manga.title
init {
@@ -100,6 +99,6 @@ class PagesThumbnailsViewModel @Inject constructor(
add(LoadingFooter(1))
}
}
thumbnails.emitValue(pages)
thumbnails.value = pages
}
}

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.remotelist.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -9,11 +8,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -21,7 +24,7 @@ import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.MangaListViewModel
@@ -65,12 +68,12 @@ class RemoteListViewModel @Inject constructor(
private val listError = MutableStateFlow<Throwable?>(null)
private var loadingJob: Job? = null
val filterItems: LiveData<List<FilterItem>>
val filterItems: StateFlow<List<FilterItem>>
get() = filter.items
override val content = combine(
mangaList,
listModeFlow,
listMode,
createHeaderFlow(),
listError,
hasNextPage,
@@ -90,7 +93,7 @@ class RemoteListViewModel @Inject constructor(
}
}
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
filter.observeState()
@@ -163,7 +166,7 @@ class RemoteListViewModel @Inject constructor(
e.printStackTraceDebug()
listError.value = e
if (!mangaList.value.isNullOrEmpty()) {
errorEvent.emitCall(e)
errorEvent.call(e)
}
}
}

View File

@@ -19,6 +19,8 @@ import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityScrobblerConfigBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
@@ -64,7 +66,7 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
viewModel.content.observe(this, listAdapter::setItems)
viewModel.user.observe(this, this::onUserChanged)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onError.observe(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onLoggedOut.observe(this) {
finishAfterTransition()
}

View File

@@ -1,23 +1,24 @@
package org.koitharu.kotatsu.scrobbling.common.ui.config
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -40,34 +41,34 @@ class ScrobblerConfigViewModel @Inject constructor(
val titleResId = scrobbler.scrobblerService.titleResId
val user = MutableLiveData<ScrobblerUser?>(null)
val onLoggedOut = SingleLiveEvent<Unit>()
val user = MutableStateFlow<ScrobblerUser?>(null)
val onLoggedOut = MutableEventFlow<Unit>()
val content = scrobbler.observeAllScrobblingInfo()
.onStart { loadingCounter.increment() }
.onFirst { loadingCounter.decrement() }
.catch { errorEvent.postCall(it) }
.catch { errorEvent.call(it) }
.map { buildContentList(it) }
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
init {
scrobbler.user
.onEach { user.emitValue(it) }
.onEach { user.value = it }
.launchIn(viewModelScope + Dispatchers.Default)
}
fun onAuthCodeReceived(authCode: String) {
launchLoadingJob(Dispatchers.Default) {
val newUser = scrobbler.authorize(authCode)
user.emitValue(newUser)
user.value = newUser
}
}
fun logout() {
launchLoadingJob(Dispatchers.Default) {
scrobbler.logout()
user.emitValue(null)
onLoggedOut.emitCall(Unit)
user.value = null
onLoggedOut.call(Unit)
}
}

View File

@@ -22,6 +22,8 @@ import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -72,8 +74,8 @@ class ScrobblingSelectorBottomSheet :
decoration.checkedItemId = it
binding.recyclerView.invalidateItemDecorations()
}
viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onClose.observe(viewLifecycleOwner) {
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
viewModel.onClose.observeEvent(viewLifecycleOwner) {
dismiss()
}
viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { index ->

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.scrobbling.common.ui.selector
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.RecyclerView.NO_ID
@@ -9,14 +7,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.SingleLiveEvent
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.emitValue
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -39,7 +40,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
val availableScrobblers = scrobblers.filter { it.isAvailable }
val selectedScrobblerIndex = MutableLiveData(0)
val selectedScrobblerIndex = MutableStateFlow(0)
private val scrobblerMangaList = MutableStateFlow<List<ScrobblerManga>>(emptyList())
private val hasNextPage = MutableStateFlow(true)
@@ -51,7 +52,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
private val currentScrobbler: Scrobbler
get() = availableScrobblers[selectedScrobblerIndex.requireValue()]
val content: LiveData<List<ListModel>> = combine(
val content: StateFlow<List<ListModel>> = combine(
scrobblerMangaList,
listError,
hasNextPage,
@@ -71,11 +72,11 @@ class ScrobblingSelectorViewModel @Inject constructor(
},
)
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
val selectedItemId = MutableLiveData(NO_ID)
val searchQuery = MutableLiveData(manga.title)
val onClose = SingleLiveEvent<Unit>()
val selectedItemId = MutableStateFlow(NO_ID)
val searchQuery = MutableStateFlow(manga.title)
val onClose = MutableEventFlow<Unit>()
val isEmpty: Boolean
get() = scrobblerMangaList.value.isEmpty()
@@ -130,13 +131,13 @@ class ScrobblingSelectorViewModel @Inject constructor(
if (doneJob?.isActive == true) {
return
}
val targetId = selectedItemId.value ?: NO_ID
val targetId = selectedItemId.value
if (targetId == NO_ID) {
onClose.call(Unit)
}
doneJob = launchJob(Dispatchers.Default) {
currentScrobbler.linkManga(manga.id, targetId)
onClose.emitCall(Unit)
onClose.call(Unit)
}
}
@@ -155,7 +156,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
try {
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
if (info != null) {
selectedItemId.emitValue(info.targetId)
selectedItemId.value = info.targetId
}
} finally {
loadList(append = false)

View File

@@ -13,6 +13,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showKeyboard
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
import org.koitharu.kotatsu.parsers.model.MangaSource

View File

@@ -7,12 +7,14 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.asFlowLiveData
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.MangaListViewModel
@@ -44,7 +46,7 @@ class SearchViewModel @Inject constructor(
override val content = combine(
mangaList,
listModeFlow,
listMode,
listError,
hasNextPage,
) { list, mode, error, hasNext ->
@@ -70,7 +72,7 @@ class SearchViewModel @Inject constructor(
result
}
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
loadList(append = false)

View File

@@ -21,6 +21,8 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
@@ -59,10 +61,8 @@ class MultiSearchActivity :
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar)
val itemCLickListener = object : OnListItemClickListener<MultiSearchListModel> {
override fun onItemClick(item: MultiSearchListModel, view: View) {
startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value))
}
val itemCLickListener = OnListItemClickListener<MultiSearchListModel> { item, view ->
startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value))
}
val sizeResolver = ItemSizeResolver(resources, settings)
val selectionDecoration = MangaSelectionDecoration(this)
@@ -90,8 +90,8 @@ class MultiSearchActivity :
viewModel.query.observe(this) { title = it }
viewModel.list.observe(this) { adapter.items = it }
viewModel.onError.observe(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onDownloadStarted.observe(this, DownloadStartedObserver(viewBinding.recyclerView))
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView))
}
override fun onWindowInsetsChanged(insets: Insets) {
@@ -130,7 +130,7 @@ class MultiSearchActivity :
}
override fun onRetryClick(error: Throwable) {
viewModel.doSearch(viewModel.query.value.orEmpty())
viewModel.doSearch(viewModel.query.value)
}
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit

Some files were not shown because too many files have changed in this diff Show More