Replace LiveData with StateFlow
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
36
app/src/main/kotlin/org/koitharu/kotatsu/core/util/Event.kt
Normal file
36
app/src/main/kotlin/org/koitharu/kotatsu/core/util/Event.kt
Normal 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)"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>? {
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user