Replace LiveData with StateFlow
This commit is contained in:
@@ -8,7 +8,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
@@ -19,7 +18,8 @@ import org.junit.runner.RunWith
|
|||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.awaitForIdle
|
import org.koitharu.kotatsu.awaitForIdle
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.backup.BackupRepository
|
|||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
|||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
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.data.PagesCache
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import javax.inject.Inject
|
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.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.ui.util.reverseAsync
|
import org.koitharu.kotatsu.core.ui.util.reverseAsync
|
||||||
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
|
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.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
@@ -81,8 +83,8 @@ class BookmarksFragment :
|
|||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui
|
package org.koitharu.kotatsu.bookmarks.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
@@ -25,9 +28,9 @@ class BookmarksViewModel @Inject constructor(
|
|||||||
private val repository: BookmarksRepository,
|
private val repository: BookmarksRepository,
|
||||||
) : BaseViewModel() {
|
) : 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 ->
|
.map { list ->
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -43,12 +46,12 @@ class BookmarksViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
|
.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>>) {
|
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val handle = repository.removeBookmarks(ids)
|
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.core.util.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
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.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
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.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ class DialogErrorObserver(
|
|||||||
fragment: Fragment?,
|
fragment: Fragment?,
|
||||||
) : this(host, fragment, null, null)
|
) : this(host, fragment, null, null)
|
||||||
|
|
||||||
override fun onChanged(value: Throwable?) {
|
override suspend fun emit(value: Throwable) {
|
||||||
if (value == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val listener = DialogListener(value)
|
val listener = DialogListener(value)
|
||||||
val dialogBuilder = MaterialAlertDialogBuilder(activity ?: host.context)
|
val dialogBuilder = MaterialAlertDialogBuilder(activity ?: host.context)
|
||||||
.setMessage(value.getDisplayMessage(host.context.resources))
|
.setMessage(value.getDisplayMessage(host.context.resources))
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||||
@@ -19,7 +19,7 @@ abstract class ErrorObserver(
|
|||||||
protected val fragment: Fragment?,
|
protected val fragment: Fragment?,
|
||||||
private val resolver: ExceptionResolver?,
|
private val resolver: ExceptionResolver?,
|
||||||
private val onResolved: Consumer<Boolean>?,
|
private val onResolved: Consumer<Boolean>?,
|
||||||
) : Observer<Throwable?> {
|
) : FlowCollector<Throwable> {
|
||||||
|
|
||||||
protected val activity = host.context.findActivity()
|
protected val activity = host.context.findActivity()
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ class SnackbarErrorObserver(
|
|||||||
fragment: Fragment?,
|
fragment: Fragment?,
|
||||||
) : this(host, fragment, null, null)
|
) : this(host, fragment, null, null)
|
||||||
|
|
||||||
override fun onChanged(value: Throwable?) {
|
override suspend fun emit(value: Throwable) {
|
||||||
if (value == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val snackbar = Snackbar.make(host, value.getDisplayMessage(host.context.resources), Snackbar.LENGTH_SHORT)
|
val snackbar = Snackbar.make(host, value.getDisplayMessage(host.context.resources), Snackbar.LENGTH_SHORT)
|
||||||
if (activity is BottomNavOwner) {
|
if (activity is BottomNavOwner) {
|
||||||
snackbar.anchorView = activity.bottomNav
|
snackbar.anchorView = activity.bottomNav
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
|
|||||||
return acc.values.max()
|
return acc.values.max()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Manga.findChapter(id: Long): MangaChapter? {
|
||||||
|
return chapters?.find { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||||
val ch = chapters
|
val ch = chapters
|
||||||
if (ch.isNullOrEmpty()) {
|
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.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
|
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
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.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Size
|
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
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.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
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 javax.inject.Inject
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
class MangaDataRepository @Inject constructor(
|
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(
|
private fun newEntity(mangaId: Long) = MangaPrefsEntity(
|
||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
mode = -1,
|
mode = -1,
|
||||||
cfBrightness = 0f,
|
cfBrightness = 0f,
|
||||||
cfContrast = 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 androidx.annotation.AnyThread
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
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.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
|||||||
@@ -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.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
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.io.File
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
import androidx.lifecycle.liveData
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
|
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
|
||||||
var lastValue: T = valueProducer()
|
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(
|
fun <T> AppSettings.observeAsStateFlow(
|
||||||
key: String,
|
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
key: String,
|
||||||
valueProducer: AppSettings.() -> T,
|
valueProducer: AppSettings.() -> T,
|
||||||
): StateFlow<T> = observe().transform {
|
): StateFlow<T> = observe().transform {
|
||||||
if (it == key) {
|
if (it == key) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
package org.koitharu.kotatsu.core.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -8,9 +7,16 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Job
|
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 kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.core.ui.util.CountedBooleanLiveData
|
import org.koitharu.kotatsu.core.util.ext.EventFlow
|
||||||
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.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
@@ -18,16 +24,17 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||||||
abstract class BaseViewModel : ViewModel() {
|
abstract class BaseViewModel : ViewModel() {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
protected val loadingCounter = CountedBooleanLiveData()
|
protected val loadingCounter = MutableStateFlow(0)
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
protected val errorEvent = SingleLiveEvent<Throwable>()
|
protected val errorEvent = MutableEventFlow<Throwable>()
|
||||||
|
|
||||||
val onError: LiveData<Throwable>
|
val onError: EventFlow<Throwable>
|
||||||
get() = errorEvent
|
get() = errorEvent
|
||||||
|
|
||||||
val isLoading: LiveData<Boolean>
|
val isLoading: StateFlow<Boolean>
|
||||||
get() = loadingCounter
|
get() = loadingCounter.map { it > 0 }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
protected fun launchJob(
|
protected fun launchJob(
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
@@ -51,7 +58,11 @@ abstract class BaseViewModel : ViewModel() {
|
|||||||
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||||
throwable.printStackTraceDebug()
|
throwable.printStackTraceDebug()
|
||||||
if (throwable !is CancellationException) {
|
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
|
package org.koitharu.kotatsu.core.ui.util
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
class ReversibleActionObserver(
|
class ReversibleActionObserver(
|
||||||
private val snackbarHost: View,
|
private val snackbarHost: View,
|
||||||
) : Observer<ReversibleAction?> {
|
) : FlowCollector<ReversibleAction> {
|
||||||
|
|
||||||
override fun onChanged(value: ReversibleAction?) {
|
override suspend fun emit(value: ReversibleAction) {
|
||||||
if (value == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val handle = value.handle
|
val handle = value.handle
|
||||||
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
||||||
val snackbar = Snackbar.make(snackbarHost, value.stringResId, length)
|
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.onCompletion
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
||||||
var isFirstCall = true
|
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.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
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.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.local.data.LocalManga
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
class DetailsInteractor @Inject constructor(
|
class DetailsInteractor @Inject constructor(
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val favouritesRepository: FavouritesRepository,
|
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> {
|
fun observeIncognitoMode(mangaFlow: Flow<Manga?>): Flow<Boolean> {
|
||||||
return mangaFlow
|
return mangaFlow
|
||||||
.distinctUntilChangedBy { it?.isNsfw }
|
.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.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
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.exception.NotFoundException
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Reusable
|
class DoubleMangaLoadUseCase @Inject constructor(
|
||||||
class DoubleMangaLoader @Inject constructor(
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
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 remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) }
|
||||||
val localDeferred = async(Dispatchers.Default) { loadLocal(manga) }
|
val localDeferred = async(Dispatchers.Default) { loadLocal(manga) }
|
||||||
DoubleManga(
|
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()
|
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()
|
val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE()
|
||||||
return load(manga)
|
return invoke(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadLocal(manga: Manga): Result<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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
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.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||||
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
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.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
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.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
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.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
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.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
|
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
|
||||||
import org.koitharu.kotatsu.core.util.ViewBadge
|
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.setNavigationBarTransparentCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||||
@@ -90,10 +93,10 @@ class DetailsActivity :
|
|||||||
ChaptersMenuProvider(viewModel, null)
|
ChaptersMenuProvider(viewModel, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.manga.observe(this, ::onMangaUpdated)
|
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
|
||||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||||
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||||
viewModel.onError.observe(
|
viewModel.onError.observeEvent(
|
||||||
this,
|
this,
|
||||||
SnackbarErrorObserver(
|
SnackbarErrorObserver(
|
||||||
host = viewBinding.containerDetails,
|
host = viewBinding.containerDetails,
|
||||||
@@ -106,11 +109,11 @@ class DetailsActivity :
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
viewModel.onShowToast.observe(this) {
|
viewModel.onShowToast.observeEvent(this) {
|
||||||
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
|
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||||
viewModel.selectedBranchName.observe(this) {
|
viewModel.selectedBranch.observe(this) {
|
||||||
viewBinding.headerChapters?.subtitle = it
|
viewBinding.headerChapters?.subtitle = it
|
||||||
viewBinding.textViewSubtitle?.textAndVisible = it
|
viewBinding.textViewSubtitle?.textAndVisible = it
|
||||||
}
|
}
|
||||||
@@ -124,7 +127,7 @@ class DetailsActivity :
|
|||||||
viewBinding.buttonDropdown.isVisible = it.size > 1
|
viewBinding.buttonDropdown.isVisible = it.size > 1
|
||||||
}
|
}
|
||||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||||
viewModel.onDownloadStarted.observe(this, DownloadStartedObserver(viewBinding.containerDetails))
|
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.containerDetails))
|
||||||
|
|
||||||
addMenuProvider(
|
addMenuProvider(
|
||||||
DetailsMenuProvider(
|
DetailsMenuProvider(
|
||||||
@@ -165,12 +168,12 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_pages_thumbs -> {
|
R.id.action_pages_thumbs -> {
|
||||||
val history = viewModel.historyInfo.value?.history
|
val history = viewModel.historyInfo.value.history
|
||||||
PagesThumbnailsSheet.show(
|
PagesThumbnailsSheet.show(
|
||||||
fm = supportFragmentManager,
|
fm = supportFragmentManager,
|
||||||
manga = viewModel.manga.value ?: return false,
|
manga = viewModel.manga.value ?: return false,
|
||||||
chapterId = history?.chapterId
|
chapterId = history?.chapterId
|
||||||
?: viewModel.chapters.value?.firstOrNull()?.chapter?.id
|
?: viewModel.chapters.value.firstOrNull()?.chapter?.id
|
||||||
?: return false,
|
?: return false,
|
||||||
currentPage = history?.page ?: 0,
|
currentPage = history?.page ?: 0,
|
||||||
)
|
)
|
||||||
@@ -253,14 +256,14 @@ class DetailsActivity :
|
|||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setTitle(R.string.translations)
|
.setTitle(R.string.translations)
|
||||||
.setItems(viewModel.branches.value.orEmpty())
|
.setItems(viewModel.branches.value)
|
||||||
.create()
|
.create()
|
||||||
.also { it.show() }
|
.also { it.show() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openReader(isIncognitoMode: Boolean) {
|
private fun openReader(isIncognitoMode: Boolean) {
|
||||||
val manga = viewModel.manga.value ?: return
|
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) {
|
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
||||||
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
|
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
|
||||||
snackbar.show()
|
snackbar.show()
|
||||||
@@ -301,11 +304,11 @@ class DetailsActivity :
|
|||||||
|
|
||||||
private class PrefetchObserver(
|
private class PrefetchObserver(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) : Observer<List<ChapterListItem>?> {
|
) : FlowCollector<List<ChapterListItem>?> {
|
||||||
|
|
||||||
private var isCalled = false
|
private var isCalled = false
|
||||||
|
|
||||||
override fun onChanged(value: List<ChapterListItem>?) {
|
override suspend fun emit(value: List<ChapterListItem>?) {
|
||||||
if (value.isNullOrEmpty()) {
|
if (value.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import coil.request.ImageRequest
|
|||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
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.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
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.resolveDp
|
||||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
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.model.HistoryInfo
|
||||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
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.image.ui.ImageActivity
|
||||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -82,7 +84,7 @@ class DetailsFragment :
|
|||||||
binding.infoLayout.textViewSource.setOnClickListener(this)
|
binding.infoLayout.textViewSource.setOnClickListener(this)
|
||||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||||
binding.chipsTags.onChipClickListener = this
|
binding.chipsTags.onChipClickListener = this
|
||||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||||
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import android.text.style.ForegroundColorSpan
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -18,9 +15,9 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -31,27 +28,28 @@ import kotlinx.coroutines.plus
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
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.ChapterListItem
|
||||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.local.data.LocalManga
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
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.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
@@ -69,7 +67,8 @@ class DetailsViewModel @Inject constructor(
|
|||||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||||
private val interactor: DetailsInteractor,
|
private val interactor: DetailsInteractor,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val mangaLoader: DoubleMangaLoader,
|
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||||
|
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
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 val doubleManga: MutableStateFlow<DoubleManga?> = MutableStateFlow(intent.manga?.let { DoubleManga(it) })
|
||||||
private var loadingJob: Job
|
private var loadingJob: Job
|
||||||
|
|
||||||
val onShowToast = SingleLiveEvent<Int>()
|
val onShowToast = MutableEventFlow<Int>()
|
||||||
val onDownloadStarted = SingleLiveEvent<Unit>()
|
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
private val mangaData = doubleManga.map { it?.any }
|
val manga = doubleManga.map { it?.any }
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.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)
|
.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)
|
.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)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||||
|
|
||||||
private val chaptersQuery = MutableStateFlow("")
|
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 }
|
val isChaptersReversed = settings.observeAsStateFlow(
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_REVERSE_CHAPTERS,
|
||||||
val manga = mangaData.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
valueProducer = { chaptersReverse },
|
||||||
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 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())
|
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
|
val localSize = doubleManga
|
||||||
.map {
|
.map {
|
||||||
@@ -128,9 +126,9 @@ class DetailsViewModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
|
||||||
|
|
||||||
val description = mangaData
|
val description = manga
|
||||||
.distinctUntilChangedBy { it?.description.orEmpty() }
|
.distinctUntilChangedBy { it?.description.orEmpty() }
|
||||||
.transformLatest {
|
.transformLatest {
|
||||||
val description = it?.description
|
val description = it?.description
|
||||||
@@ -140,16 +138,16 @@ class DetailsViewModel @Inject constructor(
|
|||||||
emit(description.parseAsHtml().filterSpans())
|
emit(description.parseAsHtml().filterSpans())
|
||||||
emit(description.parseAsHtml(imageGetter = imageGetter).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
|
val isScrobblingAvailable: Boolean
|
||||||
get() = scrobblers.any { it.isAvailable }
|
get() = scrobblers.any { it.isAvailable }
|
||||||
|
|
||||||
val scrobblingInfo: LiveData<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val branches: LiveData<List<MangaBranch>> = combine(
|
val branches: StateFlow<List<MangaBranch>> = combine(
|
||||||
doubleManga,
|
doubleManga,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
) { m, b ->
|
) { m, b ->
|
||||||
@@ -158,32 +156,29 @@ class DetailsViewModel @Inject constructor(
|
|||||||
chapters.groupBy { x -> x.branch }
|
chapters.groupBy { x -> x.branch }
|
||||||
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
||||||
.sortedWith(BranchComparator())
|
.sortedWith(BranchComparator())
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val selectedBranchName = selectedBranch
|
val isChaptersEmpty: StateFlow<Boolean> = combine(
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext, null)
|
|
||||||
|
|
||||||
val isChaptersEmpty: LiveData<Boolean> = combine(
|
|
||||||
doubleManga,
|
doubleManga,
|
||||||
isLoading.asFlow(),
|
isLoading,
|
||||||
) { manga, loading ->
|
) { manga, loading ->
|
||||||
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
|
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext, false)
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||||
|
|
||||||
val chapters = combine(
|
val chapters = combine(
|
||||||
combine(
|
combine(
|
||||||
doubleManga,
|
doubleManga,
|
||||||
history,
|
history,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
newChapters,
|
newChaptersCount,
|
||||||
) { manga, history, branch, news ->
|
) { manga, history, branch, news ->
|
||||||
mapChapters(manga?.remote, manga?.local, history, news, branch)
|
mapChapters(manga?.remote, manga?.local, history, news, branch)
|
||||||
},
|
},
|
||||||
chaptersReversed,
|
isChaptersReversed,
|
||||||
chaptersQuery,
|
chaptersQuery,
|
||||||
) { list, reversed, query ->
|
) { list, reversed, query ->
|
||||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
(if (reversed) list.asReversed() else list).filterSearch(query)
|
||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val selectedBranchValue: String?
|
val selectedBranchValue: String?
|
||||||
get() = selectedBranch.value
|
get() = selectedBranch.value
|
||||||
@@ -208,8 +203,8 @@ class DetailsViewModel @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
interactor.deleteLocalManga(m)
|
deleteLocalMangaUseCase(m)
|
||||||
onMangaRemoved.emitCall(m)
|
onMangaRemoved.call(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,12 +271,12 @@ class DetailsViewModel @Inject constructor(
|
|||||||
doubleManga.requireValue().requireAny(),
|
doubleManga.requireValue().requireAny(),
|
||||||
chaptersIds,
|
chaptersIds,
|
||||||
)
|
)
|
||||||
onDownloadStarted.emitCall(Unit)
|
onDownloadStarted.call(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||||
val result = mangaLoader.load(intent)
|
val result = doubleMangaLoadUseCase(intent)
|
||||||
val manga = result.requireAny()
|
val manga = result.requireAny()
|
||||||
// find default branch
|
// find default branch
|
||||||
val hist = historyRepository.getOne(manga)
|
val hist = historyRepository.getOne(manga)
|
||||||
@@ -317,7 +312,7 @@ class DetailsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getScrobbler(index: Int): Scrobbler? {
|
private fun getScrobbler(index: Int): Scrobbler? {
|
||||||
val info = scrobblingInfo.value?.getOrNull(index)
|
val info = scrobblingInfo.value.getOrNull(index)
|
||||||
val scrobbler = if (info != null) {
|
val scrobbler = if (info != null) {
|
||||||
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
|
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
|
||||||
} else {
|
} 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.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
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.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
|
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
|
||||||
@@ -59,7 +61,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
override fun onViewBindingCreated(binding: SheetScrobblingBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: SheetScrobblingBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
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)
|
Toast.makeText(binding.root.context, it.getDisplayMessage(binding.root.resources), Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@@ -105,7 +107,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.button_menu -> menu?.show()
|
R.id.button_menu -> menu?.show()
|
||||||
R.id.imageView_cover -> {
|
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)
|
val options = scaleUpActivityOptionsOf(v)
|
||||||
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
|
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
|
||||||
}
|
}
|
||||||
@@ -135,7 +137,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_browser -> {
|
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())
|
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent.createChooser(intent, getString(R.string.open_in_browser)),
|
Intent.createChooser(intent, getString(R.string.open_in_browser)),
|
||||||
@@ -149,7 +151,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
|
|
||||||
R.id.action_edit -> {
|
R.id.action_edit -> {
|
||||||
val manga = viewModel.manga.value ?: return false
|
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)
|
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.download.domain
|
package org.koitharu.kotatsu.download.domain
|
||||||
|
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||||
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.Manga
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ import androidx.annotation.Px
|
|||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
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.databinding.ActivityDownloadsBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.download.ui.worker.PausingReceiver
|
import org.koitharu.kotatsu.download.ui.worker.PausingReceiver
|
||||||
@@ -61,8 +63,8 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
|||||||
viewModel.items.observe(this) {
|
viewModel.items.observe(this) {
|
||||||
downloadsAdapter.items = it
|
downloadsAdapter.items = it
|
||||||
}
|
}
|
||||||
viewModel.onActionDone.observe(this, ReversibleActionObserver(viewBinding.recyclerView))
|
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView))
|
||||||
val menuObserver = Observer<Any> { _ -> invalidateOptionsMenu() }
|
val menuObserver = FlowCollector<Any> { _ -> invalidateOptionsMenu() }
|
||||||
viewModel.hasActiveWorks.observe(this, menuObserver)
|
viewModel.hasActiveWorks.observe(this, menuObserver)
|
||||||
viewModel.hasPausedWorks.observe(this, menuObserver)
|
viewModel.hasPausedWorks.observe(this, menuObserver)
|
||||||
viewModel.hasCancellableWorks.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.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
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.daysDiff
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
@@ -47,23 +47,23 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
.mapLatest { it.toDownloadsList() }
|
.mapLatest { it.toDownloadsList() }
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
|
||||||
val items = works.map {
|
val items = works.map {
|
||||||
it?.toUiList() ?: listOf(LoadingState)
|
it?.toUiList() ?: listOf(LoadingState)
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
val hasPausedWorks = works.map {
|
val hasPausedWorks = works.map {
|
||||||
it?.any { x -> x.canResume } == true
|
it?.any { x -> x.canResume } == true
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
val hasActiveWorks = works.map {
|
val hasActiveWorks = works.map {
|
||||||
it?.any { x -> x.canPause } == true
|
it?.any { x -> x.canPause } == true
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
val hasCancellableWorks = works.map {
|
val hasCancellableWorks = works.map {
|
||||||
it?.any { x -> !x.workState.isFinished } == true
|
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) {
|
fun cancel(id: UUID) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
@@ -79,14 +79,14 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
workScheduler.cancel(work.id)
|
workScheduler.cancel(work.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null))
|
onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelAll() {
|
fun cancelAll() {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
workScheduler.cancelAll()
|
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)
|
workScheduler.delete(work.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null))
|
onActionDone.call(ReversibleAction(R.string.downloads_removed, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeCompleted() {
|
fun removeCompleted() {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
workScheduler.removeCompleted()
|
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
|
package org.koitharu.kotatsu.download.ui.worker
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||||
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
|
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
|
||||||
@@ -10,9 +10,9 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
|||||||
|
|
||||||
class DownloadStartedObserver(
|
class DownloadStartedObserver(
|
||||||
private val snackbarHost: View,
|
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)
|
val snackbar = Snackbar.make(snackbarHost, R.string.download_started, Snackbar.LENGTH_LONG)
|
||||||
(snackbarHost.context.findActivity() as? BottomNavOwner)?.let {
|
(snackbarHost.context.findActivity() as? BottomNavOwner)?.let {
|
||||||
snackbar.anchorView = it.bottomNav
|
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.ext.writeAllCancellable
|
||||||
import org.koitharu.kotatsu.core.util.progress.TimeLeftEstimator
|
import org.koitharu.kotatsu.core.util.progress.TimeLeftEstimator
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
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.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
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.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.almostEquals
|
import org.koitharu.kotatsu.core.util.ext.almostEquals
|
||||||
import org.koitharu.kotatsu.core.util.ext.asArrayList
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
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.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
|
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
|
||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
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.databinding.FragmentExploreBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
|
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
|
||||||
@@ -74,9 +76,9 @@ class ExploreFragment :
|
|||||||
viewModel.content.observe(viewLifecycleOwner) {
|
viewModel.content.observe(viewLifecycleOwner) {
|
||||||
exploreAdapter?.items = it
|
exploreAdapter?.items = it
|
||||||
}
|
}
|
||||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||||
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
|
viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga)
|
||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||||
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
|
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
|
||||||
viewModel.onShowSuggestionsTip.observe(viewLifecycleOwner) {
|
viewModel.onShowSuggestionsTip.observe(viewLifecycleOwner) {
|
||||||
showSuggestionsTip()
|
showSuggestionsTip()
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui
|
package org.koitharu.kotatsu.explore.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
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.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -34,29 +35,28 @@ class ExploreViewModel @Inject constructor(
|
|||||||
private val exploreRepository: ExploreRepository,
|
private val exploreRepository: ExploreRepository,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val gridMode = settings.observeAsStateFlow(
|
val isGrid = settings.observeAsStateFlow(
|
||||||
key = AppSettings.KEY_SOURCES_GRID,
|
key = AppSettings.KEY_SOURCES_GRID,
|
||||||
scope = viewModelScope + Dispatchers.IO,
|
scope = viewModelScope + Dispatchers.IO,
|
||||||
valueProducer = { isSourcesGridMode },
|
valueProducer = { isSourcesGridMode },
|
||||||
)
|
)
|
||||||
|
|
||||||
val onOpenManga = SingleLiveEvent<Manga>()
|
val onOpenManga = MutableEventFlow<Manga>()
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
val onShowSuggestionsTip = SingleLiveEvent<Unit>()
|
val onShowSuggestionsTip = MutableEventFlow<Unit>()
|
||||||
val isGrid = gridMode.asFlowLiveData(viewModelScope.coroutineContext)
|
|
||||||
|
|
||||||
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
val content: StateFlow<List<ExploreItem>> = isLoading.flatMapLatest { loading ->
|
||||||
if (loading) {
|
if (loading) {
|
||||||
flowOf(listOf(ExploreItem.Loading))
|
flowOf(listOf(ExploreItem.Loading))
|
||||||
} else {
|
} else {
|
||||||
createContentFlow()
|
createContentFlow()
|
||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(ExploreItem.Loading))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(ExploreItem.Loading))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
if (!settings.isSuggestionsEnabled && settings.isTipEnabled(TIP_SUGGESTIONS)) {
|
if (!settings.isSuggestionsEnabled && settings.isTipEnabled(TIP_SUGGESTIONS)) {
|
||||||
onShowSuggestionsTip.emitCall(Unit)
|
onShowSuggestionsTip.call(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
fun openRandom() {
|
fun openRandom() {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
|
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
|
||||||
onOpenManga.emitCall(manga)
|
onOpenManga.call(manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
val rollback = ReversibleHandle {
|
val rollback = ReversibleHandle {
|
||||||
settings.hiddenSources -= source.name
|
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("") }
|
.onStart { emit("") }
|
||||||
.map { settings.getMangaSources(includeHidden = false) }
|
.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> {
|
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ExploreItem> {
|
||||||
val result = ArrayList<ExploreItem>(sources.size + 3)
|
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.model.FavouriteCategory
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
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.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
|
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||||
@@ -71,7 +73,7 @@ class FavouriteCategoriesActivity :
|
|||||||
onBackPressedDispatcher.addCallback(exitReorderModeCallback)
|
onBackPressedDispatcher.addCallback(exitReorderModeCallback)
|
||||||
|
|
||||||
viewModel.detalizedCategories.observe(this, ::onCategoriesChanged)
|
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)
|
viewModel.isInReorderMode.observe(this, ::onReorderModeChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories
|
package org.koitharu.kotatsu.favourites.ui.categories
|
||||||
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
|
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
|
||||||
@@ -27,23 +27,11 @@ class FavouritesCategoriesViewModel @Inject constructor(
|
|||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private var reorderJob: Job? = null
|
private var reorderJob: Job? = null
|
||||||
private val isReorder = MutableStateFlow(false)
|
val isInReorderMode = 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 detalizedCategories = combine(
|
val detalizedCategories = combine(
|
||||||
repository.observeCategoriesWithCovers(),
|
repository.observeCategoriesWithCovers(),
|
||||||
isReorder,
|
isInReorderMode,
|
||||||
) { list, reordering ->
|
) { list, reordering ->
|
||||||
list.map { (category, covers) ->
|
list.map { (category, covers) ->
|
||||||
CategoryListModel(
|
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) {
|
fun deleteCategory(id: Long) {
|
||||||
launchJob {
|
launchJob {
|
||||||
@@ -80,12 +68,12 @@ class FavouritesCategoriesViewModel @Inject constructor(
|
|||||||
settings.isAllFavouritesVisible = isVisible
|
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) {
|
fun setReorderMode(isReorderMode: Boolean) {
|
||||||
isReorder.value = isReorderMode
|
isInReorderMode.value = isReorderMode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reorderCategories(oldPos: Int, newPos: Int) {
|
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.ui.util.DefaultTextWatcher
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.core.util.ext.getSerializableCompat
|
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.core.util.ext.setChecked
|
||||||
import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding
|
import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||||
@@ -50,10 +52,10 @@ class FavouritesCategoryEditActivity :
|
|||||||
viewBinding.editName.addTextChangedListener(this)
|
viewBinding.editName.addTextChangedListener(this)
|
||||||
afterTextChanged(viewBinding.editName.text)
|
afterTextChanged(viewBinding.editName.text)
|
||||||
|
|
||||||
viewModel.onSaved.observe(this) { finishAfterTransition() }
|
viewModel.onSaved.observeEvent(this) { finishAfterTransition() }
|
||||||
viewModel.category.observe(this, ::onCategoryChanged)
|
viewModel.category.observe(this, ::onCategoryChanged)
|
||||||
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||||
viewModel.onError.observe(this, ::onError)
|
viewModel.onError.observeEvent(this, ::onError)
|
||||||
viewModel.isTrackerEnabled.observe(this) {
|
viewModel.isTrackerEnabled.observe(this) {
|
||||||
viewBinding.switchTracker.isVisible = it
|
viewBinding.switchTracker.isVisible = it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.edit
|
package org.koitharu.kotatsu.favourites.ui.categories.edit
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.liveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.model.FavouriteCategory
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.emitValue
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
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.EXTRA_ID
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.NO_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
|
private val categoryId = savedStateHandle[EXTRA_ID] ?: NO_ID
|
||||||
|
|
||||||
val onSaved = SingleLiveEvent<Unit>()
|
val onSaved = MutableEventFlow<Unit>()
|
||||||
val category = MutableLiveData<FavouriteCategory?>()
|
val category = MutableStateFlow<FavouriteCategory?>(null)
|
||||||
|
|
||||||
val isTrackerEnabled = liveData(viewModelScope.coroutineContext + Dispatchers.Default) {
|
val isTrackerEnabled = flow {
|
||||||
emit(settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources)
|
emit(settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources)
|
||||||
}
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
category.emitValue(
|
category.value = if (categoryId != NO_ID) {
|
||||||
if (categoryId != NO_ID) {
|
repository.getCategory(categoryId)
|
||||||
repository.getCategory(categoryId)
|
} else {
|
||||||
} else {
|
null
|
||||||
null
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled, isVisibleOnShelf)
|
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.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
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.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
|
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||||
@@ -46,7 +48,7 @@ class FavouriteCategoriesBottomSheet :
|
|||||||
binding.headerBar.toolbar.setOnMenuItemClickListener(this)
|
binding.headerBar.toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
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.ids
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||||
@@ -33,7 +35,7 @@ class MangaCategoriesViewModel @Inject constructor(
|
|||||||
isChecked = it.id in checked,
|
isChecked = it.id in checked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
||||||
launchJob(Dispatchers.Default) {
|
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.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
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.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.list
|
package org.koitharu.kotatsu.favourites.ui.list
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
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.ARG_CATEGORY_ID
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
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.list.domain.ListExtraProvider
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
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 categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
|
||||||
|
|
||||||
val sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
|
val sortOrder: StateFlow<SortOrder?> = if (categoryId == NO_ID) {
|
||||||
MutableLiveData(null)
|
MutableStateFlow(null)
|
||||||
} else {
|
} else {
|
||||||
repository.observeCategory(categoryId)
|
repository.observeCategory(categoryId)
|
||||||
.map { it?.order }
|
.map { it?.order }
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
@@ -57,7 +60,7 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
repository.observeAll(categoryId)
|
repository.observeAll(categoryId)
|
||||||
},
|
},
|
||||||
listModeFlow,
|
listMode,
|
||||||
) { list, mode ->
|
) { list, mode ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
@@ -77,7 +80,7 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.catch {
|
}.catch {
|
||||||
emit(listOf(it.toErrorState(canRetry = false)))
|
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
|
override fun onRefresh() = Unit
|
||||||
|
|
||||||
@@ -93,7 +96,7 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
repository.removeFromCategory(categoryId, ids)
|
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 androidx.room.withTransaction
|
||||||
import dagger.Reusable
|
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.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
||||||
import org.koitharu.kotatsu.history.data.toMangaHistory
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
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.core.model.MangaHistory
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -6,4 +6,4 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
data class MangaWithHistory(
|
data class MangaWithHistory(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val history: MangaHistory
|
val history: MangaHistory
|
||||||
)
|
)
|
||||||
@@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
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.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.history.ui
|
package org.koitharu.kotatsu.history.ui
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
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.daysDiff
|
||||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
@@ -45,15 +45,16 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler) {
|
) : MangaListViewModel(settings, downloadScheduler) {
|
||||||
|
|
||||||
val isGroupingEnabled = MutableLiveData<Boolean>()
|
val isGroupingEnabled = settings.observeAsStateFlow(
|
||||||
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled }
|
key = AppSettings.KEY_HISTORY_GROUPING,
|
||||||
.onEach { isGroupingEnabled.emitValue(it) }
|
valueProducer = { isHistoryGroupingEnabled },
|
||||||
|
)
|
||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
repository.observeAllWithHistory(),
|
repository.observeAllWithHistory(),
|
||||||
historyGrouping,
|
isGroupingEnabled,
|
||||||
listModeFlow,
|
listMode,
|
||||||
) { list, grouped, mode ->
|
) { list, grouped, mode ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
@@ -73,7 +74,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
loadingCounter.decrement()
|
loadingCounter.decrement()
|
||||||
}.catch {
|
}.catch {
|
||||||
emit(listOf(it.toErrorState(canRetry = false)))
|
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
|
override fun onRefresh() = Unit
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val handle = repository.delete(ids)
|
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 androidx.core.graphics.ColorUtils
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.scale
|
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(
|
class ReadingProgressDrawable(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.annotation.AttrRes
|
|||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
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(
|
class ReadingProgressView @JvmOverloads constructor(
|
||||||
context: Context,
|
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.clearItemDecorations
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
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.resolveDp
|
||||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||||
@@ -123,9 +125,9 @@ abstract class MangaListFragment :
|
|||||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||||
viewModel.onDownloadStarted.observe(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView))
|
viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -1,39 +1,38 @@
|
|||||||
package org.koitharu.kotatsu.list.ui
|
package org.koitharu.kotatsu.list.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.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.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
abstract class MangaListViewModel(
|
abstract class MangaListViewModel(
|
||||||
private val settings: AppSettings,
|
settings: AppSettings,
|
||||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
abstract val content: LiveData<List<ListModel>>
|
abstract val content: StateFlow<List<ListModel>>
|
||||||
protected val listModeFlow = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
|
val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, settings.listMode)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.listMode)
|
||||||
val listMode = listModeFlow.asFlowLiveData(viewModelScope.coroutineContext)
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val gridScale = settings.observeAsStateFlow(
|
||||||
val gridScale = settings.observeAsLiveData(
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
|
||||||
key = AppSettings.KEY_GRID_SIZE,
|
key = AppSettings.KEY_GRID_SIZE,
|
||||||
valueProducer = { gridSize / 100f },
|
valueProducer = { gridSize / 100f },
|
||||||
)
|
)
|
||||||
val onDownloadStarted = SingleLiveEvent<Unit>()
|
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ abstract class MangaListViewModel(
|
|||||||
fun download(items: Set<Manga>) {
|
fun download(items: Set<Manga>) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
downloadScheduler.schedule(items)
|
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.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
|
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.ItemSizeResolver
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
import org.koitharu.kotatsu.list.ui.model.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.source
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
|
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.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
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.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback
|
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.core.util.ext.parentFragmentViewModels
|
||||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
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.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
@@ -31,13 +34,13 @@ class FilterCoordinator(
|
|||||||
|
|
||||||
private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet()))
|
private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet()))
|
||||||
private var searchQuery = MutableStateFlow("")
|
private var searchQuery = MutableStateFlow("")
|
||||||
private val localTagsDeferred = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
|
private val localTags = SuspendLazy {
|
||||||
dataRepository.findTags(repository.source)
|
dataRepository.findTags(repository.source)
|
||||||
}
|
}
|
||||||
private var availableTagsDeferred = loadTagsAsync()
|
private var availableTagsDeferred = loadTagsAsync()
|
||||||
|
|
||||||
val items: LiveData<List<FilterItem>> = getItemsFlow()
|
val items: StateFlow<List<FilterItem>> = getItemsFlow()
|
||||||
.asFlowLiveData(coroutineScope.coroutineContext + Dispatchers.Default, listOf(FilterItem.Loading))
|
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(FilterItem.Loading))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeState()
|
observeState()
|
||||||
@@ -97,7 +100,7 @@ class FilterCoordinator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getTagsAsFlow() = flow {
|
private fun getTagsAsFlow() = flow {
|
||||||
val localTags = localTagsDeferred.await()
|
val localTags = localTags.get()
|
||||||
emit(TagsWrapper(localTags, isLoading = true, isError = false))
|
emit(TagsWrapper(localTags, isLoading = true, isError = false))
|
||||||
val remoteTags = tryLoadTags()
|
val remoteTags = tryLoadTags()
|
||||||
if (remoteTags == null) {
|
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.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifZero
|
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.list.domain.ListExtraProvider
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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 android.net.Uri
|
||||||
import androidx.core.net.toFile
|
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.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.util.CompositeMutex
|
import org.koitharu.kotatsu.core.util.CompositeMutex
|
||||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
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.input.LocalMangaInput
|
||||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
||||||
import org.koitharu.kotatsu.local.data.output.LocalMangaUtil
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
@@ -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.resolveName
|
||||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
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.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||||
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
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.core.util.ext.toListSorted
|
||||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
import org.koitharu.kotatsu.local.data.ImageFileFilter
|
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.MangaIndex
|
||||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.local.data.input
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toFile
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import kotlinx.coroutines.runInterruptible
|
|||||||
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||||
import org.koitharu.kotatsu.core.util.ext.readText
|
import org.koitharu.kotatsu.core.util.ext.readText
|
||||||
import org.koitharu.kotatsu.core.util.ext.toListSorted
|
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.MangaIndex
|
||||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
|||||||
@@ -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.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -38,9 +38,7 @@ class LocalManga(
|
|||||||
other as LocalManga
|
other as LocalManga
|
||||||
|
|
||||||
if (manga != other.manga) return false
|
if (manga != other.manga) return false
|
||||||
if (file != other.file) return false
|
return file == other.file
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
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.ui.CoroutineIntentService
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
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.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 org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import javax.inject.Inject
|
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.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
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.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
@@ -26,7 +27,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
|
|||||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))
|
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))
|
||||||
viewModel.onMangaRemoved.observe(viewLifecycleOwner) { onItemRemoved() }
|
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEmptyActionClick() {
|
override fun onEmptyActionClick() {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.local.ui
|
package org.koitharu.kotatsu.local.ui
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -10,18 +8,20 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
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.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
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.list.domain.ListExtraProvider
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
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.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
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.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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import java.io.IOException
|
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -49,11 +48,12 @@ class LocalListViewModel @Inject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val tagHighlighter: MangaTagHighlighter,
|
private val tagHighlighter: MangaTagHighlighter,
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||||
|
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
|
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Unit>()
|
val onMangaRemoved = MutableEventFlow<Unit>()
|
||||||
val sortOrder = MutableLiveData(settings.localListOrder)
|
val sortOrder = MutableStateFlow(settings.localListOrder)
|
||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
private val listError = MutableStateFlow<Throwable?>(null)
|
||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||||
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
|
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
|
||||||
@@ -61,8 +61,8 @@ class LocalListViewModel @Inject constructor(
|
|||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
mangaList,
|
mangaList,
|
||||||
listModeFlow,
|
listMode,
|
||||||
sortOrder.asFlow(),
|
sortOrder,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
listError,
|
listError,
|
||||||
) { list, mode, order, tags, error ->
|
) { list, mode, order, tags, error ->
|
||||||
@@ -83,7 +83,7 @@ class LocalListViewModel @Inject constructor(
|
|||||||
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
|
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
onRefresh()
|
onRefresh()
|
||||||
@@ -120,18 +120,8 @@ class LocalListViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun delete(ids: Set<Long>) {
|
fun delete(ids: Set<Long>) {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
|
deleteLocalMangaUseCase(ids)
|
||||||
for (manga in itemsToRemove) {
|
onMangaRemoved.call(Unit)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@HiltWorker
|
@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.ui.widgets.SlidingBottomNavigationView
|
||||||
import org.koitharu.kotatsu.core.util.ext.drawableEnd
|
import org.koitharu.kotatsu.core.util.ext.drawableEnd
|
||||||
import org.koitharu.kotatsu.core.util.ext.hideKeyboard
|
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.resolve
|
||||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
||||||
@@ -137,8 +139,8 @@ class MainActivity :
|
|||||||
onFirstStart()
|
onFirstStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.onOpenReader.observe(this, this::onOpenReader)
|
viewModel.onOpenReader.observeEvent(this, this::onOpenReader)
|
||||||
viewModel.onError.observe(this, SnackbarErrorObserver(viewBinding.container, null))
|
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null))
|
||||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||||
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
||||||
viewModel.counters.observe(this, ::onCountersChanged)
|
viewModel.counters.observe(this, ::onCountersChanged)
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ import androidx.core.util.set
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.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.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
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.Manga
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -24,21 +27,25 @@ import javax.inject.Inject
|
|||||||
class MainViewModel @Inject constructor(
|
class MainViewModel @Inject constructor(
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val appUpdateRepository: AppUpdateRepository,
|
private val appUpdateRepository: AppUpdateRepository,
|
||||||
private val trackingRepository: TrackingRepository,
|
trackingRepository: TrackingRepository,
|
||||||
private val settings: AppSettings,
|
settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val onOpenReader = SingleLiveEvent<Manga>()
|
val onOpenReader = MutableEventFlow<Manga>()
|
||||||
|
|
||||||
val isResumeEnabled = combine(
|
val isResumeEnabled = combine(
|
||||||
historyRepository.observeHasItems(),
|
historyRepository.observeHasItems(),
|
||||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
||||||
) { hasItems, incognito ->
|
) { hasItems, incognito ->
|
||||||
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(
|
val isFeedAvailable = settings.observeAsStateFlow(
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_TRACKER_ENABLED,
|
key = AppSettings.KEY_TRACKER_ENABLED,
|
||||||
valueProducer = { isTrackerEnabled },
|
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_tools] = if (appUpdate != null) 1 else 0
|
||||||
a[R.id.nav_feed] = tracks
|
a[R.id.nav_feed] = tracks
|
||||||
a
|
a
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0))
|
}.stateIn(
|
||||||
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = SparseIntArray(0),
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchJob {
|
launchJob {
|
||||||
@@ -62,7 +73,7 @@ class MainViewModel @Inject constructor(
|
|||||||
fun openLastReader() {
|
fun openLastReader() {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
|
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.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
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
|
import org.koitharu.kotatsu.databinding.ActivityProtectBinding
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -42,9 +44,9 @@ class ProtectActivity :
|
|||||||
viewBinding.buttonNext.setOnClickListener(this)
|
viewBinding.buttonNext.setOnClickListener(this)
|
||||||
viewBinding.buttonCancel.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.isLoading.observe(this, this::onLoadingStateChanged)
|
||||||
viewModel.onUnlockSuccess.observe(this) {
|
viewModel.onUnlockSuccess.observeEvent(this) {
|
||||||
val intent = intent.getParcelableExtraCompat<Intent>(EXTRA_INTENT)
|
val intent = intent.getParcelableExtraCompat<Intent>(EXTRA_INTENT)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import kotlinx.coroutines.delay
|
|||||||
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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 org.koitharu.kotatsu.parsers.util.md5
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class ProtectViewModel @Inject constructor(
|
|||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
val onUnlockSuccess = SingleLiveEvent<Unit>()
|
val onUnlockSuccess = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
val isBiometricEnabled
|
val isBiometricEnabled
|
||||||
get() = settings.isBiometricProtectionEnabled
|
get() = settings.isBiometricProtectionEnabled
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import android.util.LongSparseArray
|
|||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
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.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import javax.inject.Inject
|
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
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
@@ -15,7 +16,6 @@ import okio.IOException
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||||
@@ -74,7 +74,7 @@ class PageSaveHelper @Inject constructor(
|
|||||||
var extension = name.substringAfterLast('.', "")
|
var extension = name.substringAfterLast('.', "")
|
||||||
name = name.substringBeforeLast('.')
|
name = name.substringBeforeLast('.')
|
||||||
if (extension.length !in 2..4) {
|
if (extension.length !in 2..4) {
|
||||||
val mimeType = MangaDataRepository.getImageMimeType(file)
|
val mimeType = getImageMimeType(file)
|
||||||
extension = if (mimeType != null) {
|
extension = if (mimeType != null) {
|
||||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
||||||
} else {
|
} else {
|
||||||
@@ -83,4 +83,12 @@ class PageSaveHelper @Inject constructor(
|
|||||||
}
|
}
|
||||||
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
|
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.ShareHelper
|
||||||
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
|
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
|
||||||
import org.koitharu.kotatsu.core.util.ext.isRtl
|
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.postDelayed
|
||||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
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.databinding.ActivityReaderBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
@@ -108,7 +110,7 @@ class ReaderActivity :
|
|||||||
insetsDelegate.interceptingWindowInsetsListener = this
|
insetsDelegate.interceptingWindowInsetsListener = this
|
||||||
idlingDetector.bindToLifecycle(this)
|
idlingDetector.bindToLifecycle(this)
|
||||||
|
|
||||||
viewModel.onError.observe(
|
viewModel.onError.observeEvent(
|
||||||
this,
|
this,
|
||||||
DialogErrorObserver(
|
DialogErrorObserver(
|
||||||
host = viewBinding.container,
|
host = viewBinding.container,
|
||||||
@@ -117,23 +119,23 @@ class ReaderActivity :
|
|||||||
onResolved = { isResolved ->
|
onResolved = { isResolved ->
|
||||||
if (isResolved) {
|
if (isResolved) {
|
||||||
viewModel.reload()
|
viewModel.reload()
|
||||||
} else if (viewModel.content.value?.pages.isNullOrEmpty()) {
|
} else if (viewModel.content.value.pages.isEmpty()) {
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
viewModel.readerMode.observe(this, this::onInitReader)
|
viewModel.readerMode.observe(this, this::onInitReader)
|
||||||
viewModel.onPageSaved.observe(this, this::onPageSaved)
|
viewModel.onPageSaved.observeEvent(this, this::onPageSaved)
|
||||||
viewModel.uiState.observeWithPrevious(this, this::onUiStateChanged)
|
viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
|
||||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||||
viewModel.content.observe(this) {
|
viewModel.content.observe(this) {
|
||||||
onLoadingStateChanged(viewModel.isLoading.value == true)
|
onLoadingStateChanged(viewModel.isLoading.value)
|
||||||
}
|
}
|
||||||
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
|
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
|
||||||
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
|
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
|
||||||
viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
|
viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
|
||||||
viewModel.onShowToast.observe(this) { msgId ->
|
viewModel.onShowToast.observeEvent(this) { msgId ->
|
||||||
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
|
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
|
||||||
.setAnchorView(viewBinding.appbarBottom)
|
.setAnchorView(viewBinding.appbarBottom)
|
||||||
.show()
|
.show()
|
||||||
@@ -150,7 +152,10 @@ class ReaderActivity :
|
|||||||
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInitReader(mode: ReaderMode) {
|
private fun onInitReader(mode: ReaderMode?) {
|
||||||
|
if (mode == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (readerManager.currentMode != mode) {
|
if (readerManager.currentMode != mode) {
|
||||||
readerManager.replace(mode)
|
readerManager.replace(mode)
|
||||||
}
|
}
|
||||||
@@ -190,7 +195,7 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_bookmark -> {
|
R.id.action_bookmark -> {
|
||||||
if (viewModel.isBookmarkAdded.value == true) {
|
if (viewModel.isBookmarkAdded.value) {
|
||||||
viewModel.removeBookmark()
|
viewModel.removeBookmark()
|
||||||
} else {
|
} else {
|
||||||
viewModel.addBookmark()
|
viewModel.addBookmark()
|
||||||
@@ -209,7 +214,7 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
|
val hasPages = viewModel.content.value.pages.isNotEmpty()
|
||||||
viewBinding.layoutLoading.isVisible = isLoading && !hasPages
|
viewBinding.layoutLoading.isVisible = isLoading && !hasPages
|
||||||
if (isLoading && hasPages) {
|
if (isLoading && hasPages) {
|
||||||
viewBinding.toastView.show(R.string.loading_)
|
viewBinding.toastView.show(R.string.loading_)
|
||||||
@@ -260,7 +265,7 @@ class ReaderActivity :
|
|||||||
|
|
||||||
override fun onPageSelected(page: ReaderPage) {
|
override fun onPageSelected(page: ReaderPage) {
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
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 }
|
val index = pages.indexOfFirst { it.chapterId == page.chapterId && it.id == page.id }
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -311,7 +316,7 @@ class ReaderActivity :
|
|||||||
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
|
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
|
||||||
viewBinding.appbarTop.isVisible = isUiVisible
|
viewBinding.appbarTop.isVisible = isUiVisible
|
||||||
viewBinding.appbarBottom?.isVisible = isUiVisible
|
viewBinding.appbarBottom?.isVisible = isUiVisible
|
||||||
viewBinding.infoBar.isGone = isUiVisible || (viewModel.isInfoBarEnabled.value == false)
|
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
|
||||||
if (isUiVisible) {
|
if (isUiVisible) {
|
||||||
showSystemUI()
|
showSystemUI()
|
||||||
} else {
|
} else {
|
||||||
@@ -367,7 +372,8 @@ class ReaderActivity :
|
|||||||
menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
|
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_)
|
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
|
||||||
viewBinding.infoBar.update(uiState)
|
viewBinding.infoBar.update(uiState)
|
||||||
if (uiState == null) {
|
if (uiState == null) {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -28,35 +26,32 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
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.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||||
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
|
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.exception.NotFoundException
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
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.ChaptersLoader
|
||||||
|
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||||
@@ -70,7 +65,6 @@ private const val PREFETCH_LIMIT = 10
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ReaderViewModel @Inject constructor(
|
class ReaderViewModel @Inject constructor(
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
private val dataRepository: MangaDataRepository,
|
private val dataRepository: MangaDataRepository,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val bookmarksRepository: BookmarksRepository,
|
private val bookmarksRepository: BookmarksRepository,
|
||||||
@@ -79,7 +73,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
private val pageLoader: PageLoader,
|
private val pageLoader: PageLoader,
|
||||||
private val chaptersLoader: ChaptersLoader,
|
private val chaptersLoader: ChaptersLoader,
|
||||||
private val shortcutsUpdater: ShortcutsUpdater,
|
private val shortcutsUpdater: ShortcutsUpdater,
|
||||||
private val mangaLoader: DoubleMangaLoader,
|
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||||
|
private val historyUpdateUseCase: HistoryUpdateUseCase,
|
||||||
|
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
private val intent = MangaIntent(savedStateHandle)
|
||||||
@@ -95,29 +91,29 @@ class ReaderViewModel @Inject constructor(
|
|||||||
private val mangaFlow: Flow<Manga?>
|
private val mangaFlow: Flow<Manga?>
|
||||||
get() = mangaData.map { it?.any }
|
get() = mangaData.map { it?.any }
|
||||||
|
|
||||||
val readerMode = MutableLiveData<ReaderMode>()
|
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
||||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
val onPageSaved = MutableEventFlow<Uri?>()
|
||||||
val onShowToast = SingleLiveEvent<Int>()
|
val onShowToast = MutableEventFlow<Int>()
|
||||||
val uiState = MutableLiveData<ReaderUiState?>(null)
|
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
||||||
|
|
||||||
val content = MutableLiveData(ReaderContent(emptyList(), null))
|
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
||||||
val manga: DoubleManga?
|
val manga: DoubleManga?
|
||||||
get() = mangaData.value
|
get() = mangaData.value
|
||||||
|
|
||||||
val readerAnimation = settings.observeAsLiveData(
|
val readerAnimation = settings.observeAsStateFlow(
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_READER_ANIMATION,
|
key = AppSettings.KEY_READER_ANIMATION,
|
||||||
valueProducer = { readerAnimation },
|
valueProducer = { readerAnimation },
|
||||||
)
|
)
|
||||||
|
|
||||||
val isInfoBarEnabled = settings.observeAsLiveData(
|
val isInfoBarEnabled = settings.observeAsStateFlow(
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_READER_BAR,
|
key = AppSettings.KEY_READER_BAR,
|
||||||
valueProducer = { isReaderBarEnabled },
|
valueProducer = { isReaderBarEnabled },
|
||||||
)
|
)
|
||||||
|
|
||||||
val isWebtoonZoomEnabled = settings.observeAsLiveData(
|
val isWebtoonZoomEnabled = settings.observeAsStateFlow(
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_WEBTOON_ZOOM,
|
key = AppSettings.KEY_WEBTOON_ZOOM,
|
||||||
valueProducer = { isWebtoonZoomEnable },
|
valueProducer = { isWebtoonZoomEnable },
|
||||||
)
|
)
|
||||||
@@ -136,9 +132,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
) { manga, policy ->
|
) { manga, policy ->
|
||||||
policy == ScreenshotsPolicy.BLOCK_ALL ||
|
policy == ScreenshotsPolicy.BLOCK_ALL ||
|
||||||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
|
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
|
||||||
}.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
|
val manga = mangaData.value?.any
|
||||||
if (state == null || manga == null) {
|
if (state == null || manga == null) {
|
||||||
flowOf(false)
|
flowOf(false)
|
||||||
@@ -146,7 +142,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
bookmarksRepository.observeBookmark(manga, state.chapterId, state.page)
|
bookmarksRepository.observeBookmark(manga, state.chapterId, state.page)
|
||||||
.map { it != null }
|
.map { it != null }
|
||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadImpl()
|
loadImpl()
|
||||||
@@ -173,10 +169,8 @@ class ReaderViewModel @Inject constructor(
|
|||||||
mode = newMode,
|
mode = newMode,
|
||||||
)
|
)
|
||||||
readerMode.value = newMode
|
readerMode.value = newMode
|
||||||
content.value?.run {
|
content.update {
|
||||||
content.value = copy(
|
it.copy(state = getCurrentState())
|
||||||
state = getCurrentState(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,9 +183,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val readerState = state ?: currentState.value ?: return
|
val readerState = state ?: currentState.value ?: return
|
||||||
historyRepository.saveStateAsync(
|
historyUpdateUseCase.invokeAsync(
|
||||||
manga = mangaData.value?.any ?: return,
|
manga = mangaData.value?.any ?: return,
|
||||||
state = readerState,
|
readerState = readerState,
|
||||||
percent = computePercent(readerState.chapterId, readerState.page),
|
percent = computePercent(readerState.chapterId, readerState.page),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -212,12 +206,12 @@ class ReaderViewModel @Inject constructor(
|
|||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
try {
|
try {
|
||||||
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
|
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
|
||||||
onPageSaved.emitCall(dest)
|
onPageSaved.call(dest)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
onPageSaved.emitCall(null)
|
onPageSaved.call(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +227,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun getCurrentPage(): MangaPage? {
|
fun getCurrentPage(): MangaPage? {
|
||||||
val state = currentState.value ?: return null
|
val state = currentState.value ?: return null
|
||||||
return content.value?.pages?.find {
|
return content.value.pages.find {
|
||||||
it.chapterId == state.chapterId && it.index == state.page
|
it.chapterId == state.chapterId && it.index == state.page
|
||||||
}?.toMangaPage()
|
}?.toMangaPage()
|
||||||
}
|
}
|
||||||
@@ -242,9 +236,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val prevJob = loadingJob
|
val prevJob = loadingJob
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
content.postValue(ReaderContent(emptyList(), null))
|
content.value = ReaderContent(emptyList(), null)
|
||||||
chaptersLoader.loadSingleChapter(id)
|
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) {
|
stateChangeJob = launchJob(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
loadingJob?.join()
|
loadingJob?.join()
|
||||||
val pages = content.value?.pages ?: return@launchJob
|
val pages = content.value.pages
|
||||||
pages.getOrNull(position)?.let { page ->
|
pages.getOrNull(position)?.let { page ->
|
||||||
currentState.update { cs ->
|
currentState.update { cs ->
|
||||||
cs?.copy(chapterId = page.chapterId, page = page.index)
|
cs?.copy(chapterId = page.chapterId, page = page.index)
|
||||||
@@ -296,7 +290,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
percent = computePercent(state.chapterId, state.page),
|
percent = computePercent(state.chapterId, state.page),
|
||||||
)
|
)
|
||||||
bookmarksRepository.addBookmark(bookmark)
|
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 =
|
var manga =
|
||||||
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", ""))
|
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", ""))
|
||||||
mangaData.value = manga
|
mangaData.value = manga
|
||||||
manga = mangaLoader.load(intent)
|
manga = doubleMangaLoadUseCase(intent)
|
||||||
chaptersLoader.init(manga)
|
chaptersLoader.init(manga)
|
||||||
// determine mode
|
// determine mode
|
||||||
val singleManga = manga.requireAny()
|
val singleManga = manga.requireAny()
|
||||||
val mode = detectReaderMode(singleManga)
|
|
||||||
// obtain state
|
// obtain state
|
||||||
if (currentState.value == null) {
|
if (currentState.value == null) {
|
||||||
currentState.value = historyRepository.getOne(singleManga)?.let {
|
currentState.value = historyRepository.getOne(singleManga)?.let {
|
||||||
ReaderState(it)
|
ReaderState(it)
|
||||||
} ?: ReaderState(singleManga, preselectedBranch)
|
} ?: ReaderState(singleManga, preselectedBranch)
|
||||||
}
|
}
|
||||||
|
val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value)
|
||||||
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
||||||
mangaData.value = manga.filterChapters(branch)
|
mangaData.value = manga.filterChapters(branch)
|
||||||
readerMode.emitValue(mode)
|
readerMode.value = mode
|
||||||
|
|
||||||
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
||||||
// save state
|
// save state
|
||||||
if (!isIncognito) {
|
if (!isIncognito) {
|
||||||
currentState.value?.let {
|
currentState.value?.let {
|
||||||
val percent = computePercent(it.chapterId, it.page)
|
val percent = computePercent(it.chapterId, it.page)
|
||||||
historyRepository.addOrUpdate(singleManga, it.chapterId, it.page, it.scroll, percent)
|
historyUpdateUseCase.invoke(singleManga, it, percent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyStateChanged()
|
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) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
prevJob?.join()
|
prevJob?.join()
|
||||||
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
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
|
@WorkerThread
|
||||||
private fun notifyStateChanged() {
|
private fun notifyStateChanged() {
|
||||||
val state = getCurrentState()
|
val state = getCurrentState()
|
||||||
@@ -402,7 +374,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
isSliderEnabled = settings.isReaderSliderEnabled,
|
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||||
percent = if (state != null) computePercent(state.chapterId, state.page) else PROGRESS_NONE,
|
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 {
|
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
||||||
@@ -419,23 +391,3 @@ class ReaderViewModel @Inject constructor(
|
|||||||
return ppc * chapterIndex + ppc * pagePercent
|
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.decodeRegion
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.indicator
|
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.core.util.ext.setValueRounded
|
||||||
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
|
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -64,7 +66,7 @@ class ColorFilterConfigActivity :
|
|||||||
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
|
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
|
||||||
viewModel.isLoading.observe(this, this::onLoadingChanged)
|
viewModel.isLoading.observe(this, this::onLoadingChanged)
|
||||||
viewModel.preview.observe(this, this::onPreviewChanged)
|
viewModel.preview.observe(this, this::onPreviewChanged)
|
||||||
viewModel.onDismiss.observe(this) {
|
viewModel.onDismiss.observeEvent(this) {
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.DialogInterface
|
|||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
|
||||||
class ColorFilterConfigBackPressedDispatcher(
|
class ColorFilterConfigBackPressedDispatcher(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.colorfilter
|
package org.koitharu.kotatsu.reader.ui.colorfilter
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.emitValue
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
|
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 val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
|
||||||
|
|
||||||
private var initialColorFilter: ReaderColorFilter? = null
|
private var initialColorFilter: ReaderColorFilter? = null
|
||||||
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
|
val colorFilter = MutableStateFlow<ReaderColorFilter?>(null)
|
||||||
val onDismiss = SingleLiveEvent<Unit>()
|
val onDismiss = MutableEventFlow<Unit>()
|
||||||
val preview = MutableLiveData<MangaPage?>(null)
|
val preview = MutableStateFlow<MangaPage?>(null)
|
||||||
|
|
||||||
val isChanged: Boolean
|
val isChanged: Boolean
|
||||||
get() = colorFilter.value != initialColorFilter
|
get() = colorFilter.value != initialColorFilter
|
||||||
@@ -44,13 +44,11 @@ class ColorFilterConfigViewModel @Inject constructor(
|
|||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val repository = mangaRepositoryFactory.create(page.source)
|
val repository = mangaRepositoryFactory.create(page.source)
|
||||||
val url = repository.getPageUrl(page)
|
val url = repository.getPageUrl(page)
|
||||||
preview.emitValue(
|
preview.value = MangaPage(
|
||||||
MangaPage(
|
id = page.id,
|
||||||
id = page.id,
|
url = url,
|
||||||
url = url,
|
preview = page.preview,
|
||||||
preview = page.preview,
|
source = page.source,
|
||||||
source = page.source,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +70,7 @@ class ColorFilterConfigViewModel @Inject constructor(
|
|||||||
fun save() {
|
fun save() {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
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.ui.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
|
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.viewLifecycleScope
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
|
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
|
||||||
@@ -75,8 +77,8 @@ class ReaderConfigBottomSheet :
|
|||||||
binding.sliderTimer.addOnChangeListener(this)
|
binding.sliderTimer.addOnChangeListener(this)
|
||||||
binding.switchScrollTimer.setOnCheckedChangeListener(this)
|
binding.switchScrollTimer.setOnCheckedChangeListener(this)
|
||||||
|
|
||||||
settings.observeAsLiveData(
|
settings.observeAsStateFlow(
|
||||||
context = lifecycleScope.coroutineContext + Dispatchers.Default,
|
scope = lifecycleScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
|
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
|
||||||
valueProducer = { readerAutoscrollSpeed },
|
valueProducer = { readerAutoscrollSpeed },
|
||||||
).observe(viewLifecycleOwner) {
|
).observe(viewLifecycleOwner) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
|
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.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
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.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
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.recyclerView
|
||||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
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.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
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.recyclerView
|
||||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
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.findCenterViewPosition
|
||||||
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
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.core.util.ext.viewLifecycleScope
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
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.ScrollListenerInvalidationObserver
|
||||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
|
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.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||||
@@ -93,7 +95,7 @@ class PagesThumbnailsSheet :
|
|||||||
viewModel.branch.observe(viewLifecycleOwner) {
|
viewModel.branch.observe(viewLifecycleOwner) {
|
||||||
onExpansionStateChanged(binding.headerBar, binding.headerBar.isExpanded)
|
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() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
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.parsers.util.SuspendLazy
|
||||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -22,7 +21,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val chaptersLoader: ChaptersLoader,
|
private val chaptersLoader: ChaptersLoader,
|
||||||
private val mangaLoader: DoubleMangaLoader,
|
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
|
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 repository = mangaRepositoryFactory.create(manga.source)
|
||||||
private val mangaDetails = SuspendLazy {
|
private val mangaDetails = SuspendLazy {
|
||||||
mangaLoader.load(manga).let {
|
doubleMangaLoadUseCase(manga).let {
|
||||||
val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch
|
val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch
|
||||||
branch.emitValue(b)
|
branch.value = b
|
||||||
it.filterChapters(b)
|
it.filterChapters(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,8 +40,8 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
private var loadingPrevJob: Job? = null
|
private var loadingPrevJob: Job? = null
|
||||||
private var loadingNextJob: Job? = null
|
private var loadingNextJob: Job? = null
|
||||||
|
|
||||||
val thumbnails = MutableLiveData<List<ListModel>>()
|
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
|
||||||
val branch = MutableLiveData<String?>()
|
val branch = MutableStateFlow<String?>(null)
|
||||||
val title = manga.title
|
val title = manga.title
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -100,6 +99,6 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
add(LoadingFooter(1))
|
add(LoadingFooter(1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thumbnails.emitValue(pages)
|
thumbnails.value = pages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.remotelist.ui
|
package org.koitharu.kotatsu.remotelist.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -9,11 +8,15 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
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.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
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.core.util.ext.require
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
@@ -65,12 +68,12 @@ class RemoteListViewModel @Inject constructor(
|
|||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
private val listError = MutableStateFlow<Throwable?>(null)
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
|
|
||||||
val filterItems: LiveData<List<FilterItem>>
|
val filterItems: StateFlow<List<FilterItem>>
|
||||||
get() = filter.items
|
get() = filter.items
|
||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
mangaList,
|
mangaList,
|
||||||
listModeFlow,
|
listMode,
|
||||||
createHeaderFlow(),
|
createHeaderFlow(),
|
||||||
listError,
|
listError,
|
||||||
hasNextPage,
|
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 {
|
init {
|
||||||
filter.observeState()
|
filter.observeState()
|
||||||
@@ -163,7 +166,7 @@ class RemoteListViewModel @Inject constructor(
|
|||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
listError.value = e
|
listError.value = e
|
||||||
if (!mangaList.value.isNullOrEmpty()) {
|
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.disposeImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
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.databinding.ActivityScrobblerConfigBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||||
@@ -64,7 +66,7 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
|
|||||||
viewModel.content.observe(this, listAdapter::setItems)
|
viewModel.content.observe(this, listAdapter::setItems)
|
||||||
viewModel.user.observe(this, this::onUserChanged)
|
viewModel.user.observe(this, this::onUserChanged)
|
||||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
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) {
|
viewModel.onLoggedOut.observe(this) {
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
import org.koitharu.kotatsu.core.util.ext.require
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
@@ -40,34 +41,34 @@ class ScrobblerConfigViewModel @Inject constructor(
|
|||||||
|
|
||||||
val titleResId = scrobbler.scrobblerService.titleResId
|
val titleResId = scrobbler.scrobblerService.titleResId
|
||||||
|
|
||||||
val user = MutableLiveData<ScrobblerUser?>(null)
|
val user = MutableStateFlow<ScrobblerUser?>(null)
|
||||||
val onLoggedOut = SingleLiveEvent<Unit>()
|
val onLoggedOut = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
val content = scrobbler.observeAllScrobblingInfo()
|
val content = scrobbler.observeAllScrobblingInfo()
|
||||||
.onStart { loadingCounter.increment() }
|
.onStart { loadingCounter.increment() }
|
||||||
.onFirst { loadingCounter.decrement() }
|
.onFirst { loadingCounter.decrement() }
|
||||||
.catch { errorEvent.postCall(it) }
|
.catch { errorEvent.call(it) }
|
||||||
.map { buildContentList(it) }
|
.map { buildContentList(it) }
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scrobbler.user
|
scrobbler.user
|
||||||
.onEach { user.emitValue(it) }
|
.onEach { user.value = it }
|
||||||
.launchIn(viewModelScope + Dispatchers.Default)
|
.launchIn(viewModelScope + Dispatchers.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAuthCodeReceived(authCode: String) {
|
fun onAuthCodeReceived(authCode: String) {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val newUser = scrobbler.authorize(authCode)
|
val newUser = scrobbler.authorize(authCode)
|
||||||
user.emitValue(newUser)
|
user.value = newUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
scrobbler.logout()
|
scrobbler.logout()
|
||||||
user.emitValue(null)
|
user.value = null
|
||||||
onLoggedOut.emitCall(Unit)
|
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.ui.util.CollapseActionViewCallback
|
||||||
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
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.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
|
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
@@ -72,8 +74,8 @@ class ScrobblingSelectorBottomSheet :
|
|||||||
decoration.checkedItemId = it
|
decoration.checkedItemId = it
|
||||||
binding.recyclerView.invalidateItemDecorations()
|
binding.recyclerView.invalidateItemDecorations()
|
||||||
}
|
}
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
|
||||||
viewModel.onClose.observe(viewLifecycleOwner) {
|
viewModel.onClose.observeEvent(viewLifecycleOwner) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { index ->
|
viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { index ->
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector
|
package org.koitharu.kotatsu.scrobbling.common.ui.selector
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
@@ -9,14 +7,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
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.asFlowLiveData
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
import org.koitharu.kotatsu.core.util.ext.require
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
@@ -39,7 +40,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
|||||||
|
|
||||||
val availableScrobblers = scrobblers.filter { it.isAvailable }
|
val availableScrobblers = scrobblers.filter { it.isAvailable }
|
||||||
|
|
||||||
val selectedScrobblerIndex = MutableLiveData(0)
|
val selectedScrobblerIndex = MutableStateFlow(0)
|
||||||
|
|
||||||
private val scrobblerMangaList = MutableStateFlow<List<ScrobblerManga>>(emptyList())
|
private val scrobblerMangaList = MutableStateFlow<List<ScrobblerManga>>(emptyList())
|
||||||
private val hasNextPage = MutableStateFlow(true)
|
private val hasNextPage = MutableStateFlow(true)
|
||||||
@@ -51,7 +52,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
|||||||
private val currentScrobbler: Scrobbler
|
private val currentScrobbler: Scrobbler
|
||||||
get() = availableScrobblers[selectedScrobblerIndex.requireValue()]
|
get() = availableScrobblers[selectedScrobblerIndex.requireValue()]
|
||||||
|
|
||||||
val content: LiveData<List<ListModel>> = combine(
|
val content: StateFlow<List<ListModel>> = combine(
|
||||||
scrobblerMangaList,
|
scrobblerMangaList,
|
||||||
listError,
|
listError,
|
||||||
hasNextPage,
|
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 selectedItemId = MutableStateFlow(NO_ID)
|
||||||
val searchQuery = MutableLiveData(manga.title)
|
val searchQuery = MutableStateFlow(manga.title)
|
||||||
val onClose = SingleLiveEvent<Unit>()
|
val onClose = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = scrobblerMangaList.value.isEmpty()
|
get() = scrobblerMangaList.value.isEmpty()
|
||||||
@@ -130,13 +131,13 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
|||||||
if (doneJob?.isActive == true) {
|
if (doneJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val targetId = selectedItemId.value ?: NO_ID
|
val targetId = selectedItemId.value
|
||||||
if (targetId == NO_ID) {
|
if (targetId == NO_ID) {
|
||||||
onClose.call(Unit)
|
onClose.call(Unit)
|
||||||
}
|
}
|
||||||
doneJob = launchJob(Dispatchers.Default) {
|
doneJob = launchJob(Dispatchers.Default) {
|
||||||
currentScrobbler.linkManga(manga.id, targetId)
|
currentScrobbler.linkManga(manga.id, targetId)
|
||||||
onClose.emitCall(Unit)
|
onClose.call(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
|
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
selectedItemId.emitValue(info.targetId)
|
selectedItemId.value = info.targetId
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loadList(append = false)
|
loadList(append = false)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
|
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.core.util.ext.showKeyboard
|
||||||
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
|
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user